cracked
This commit is contained in:
parent
481e07bddd
commit
0006ddad3d
|
|
@ -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
|
||||
|
|
@ -68,6 +68,16 @@ jobs:
|
|||
- name: dlp-scanner
|
||||
type: ruff
|
||||
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)
|
||||
- name: bug-bounty-platform-frontend
|
||||
type: biome
|
||||
|
|
@ -84,6 +94,21 @@ jobs:
|
|||
- name: encrypted-p2p-chat-frontend
|
||||
type: biome
|
||||
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
|
||||
- name: simple-vulnerability-scanner
|
||||
type: go
|
||||
|
|
@ -94,10 +119,51 @@ jobs:
|
|||
- name: secrets-scanner
|
||||
type: go
|
||||
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
|
||||
- name: credential-enumeration
|
||||
type: nim
|
||||
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:
|
||||
run:
|
||||
|
|
@ -168,6 +234,61 @@ jobs:
|
|||
if: matrix.type == 'nim'
|
||||
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
|
||||
- name: Run ruff
|
||||
if: matrix.type == 'ruff'
|
||||
|
|
@ -247,6 +368,152 @@ jobs:
|
|||
cat nim-output.txt
|
||||
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
|
||||
- name: Create Ruff Lint Summary
|
||||
if: matrix.type == 'ruff'
|
||||
|
|
@ -371,6 +638,151 @@ jobs:
|
|||
fi
|
||||
} >> $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
|
||||
- name: Check lint status
|
||||
run: |
|
||||
|
|
@ -394,5 +806,30 @@ jobs:
|
|||
echo "Nim lint checks failed"
|
||||
exit 1
|
||||
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
|
||||
echo "All lint checks passed"
|
||||
|
|
|
|||
|
|
@ -84,6 +84,37 @@ repos:
|
|||
files: ^PROJECTS/advanced/ai-threat-detection/backend/
|
||||
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
|
||||
- repo: local
|
||||
|
|
@ -112,6 +143,48 @@ repos:
|
|||
types: [go]
|
||||
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
|
||||
- repo: local
|
||||
hooks:
|
||||
|
|
@ -150,6 +223,41 @@ repos:
|
|||
files: ^PROJECTS/advanced/encrypted-p2p-chat/frontend/src/
|
||||
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
|
||||
- repo: local
|
||||
hooks:
|
||||
|
|
@ -160,6 +268,130 @@ repos:
|
|||
files: ^PROJECTS/intermediate/credential-enumeration/src/
|
||||
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
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// event-feed.tsx
|
||||
|
||||
import { useWebSocketStore } from '@/core/lib/websocket.store'
|
||||
import { ServiceBadge } from './service-badge'
|
||||
import styles from './event-feed.module.scss'
|
||||
import { ServiceBadge } from './service-badge'
|
||||
|
||||
const MAX_VISIBLE = 15
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,5 @@ interface ServiceBadgeProps {
|
|||
}
|
||||
|
||||
export function ServiceBadge({ service }: ServiceBadgeProps) {
|
||||
return (
|
||||
<span className={styles.badge}>
|
||||
{service.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
return <span className={styles.badge}>{service.toUpperCase()}</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@ export function Shell() {
|
|||
|
||||
<div className={styles.status}>
|
||||
<span className={styles.statusLabel}>LINK</span>
|
||||
<span
|
||||
className={connected ? styles.statusOn : styles.statusOff}
|
||||
>
|
||||
<span className={connected ? styles.statusOn : styles.statusOff}>
|
||||
{connected ? 'ACTIVE' : 'DOWN'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,9 +57,7 @@ export function AttackerDetailPage() {
|
|||
|
||||
<div className={styles.dossierRow}>
|
||||
<span className={styles.dossierLabel}>THREAT SCORE</span>
|
||||
<span
|
||||
className={`${styles.dossierValue} ${styles.threatValue}`}
|
||||
>
|
||||
<span className={`${styles.dossierValue} ${styles.threatValue}`}>
|
||||
{attacker.threat_score}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ export function AttackersPage() {
|
|||
{attackers?.map((a) => (
|
||||
<tr key={a.id}>
|
||||
<td>
|
||||
<Link
|
||||
to={`/attackers/${a.id}`}
|
||||
className={styles.link}
|
||||
>
|
||||
<Link to={`/attackers/${a.id}`} className={styles.link}>
|
||||
{a.ip}
|
||||
</Link>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -113,11 +113,7 @@ export function DashboardPage() {
|
|||
axisLine={false}
|
||||
/>
|
||||
<Tooltip contentStyle={TOOLTIP_STYLE} cursor={false} />
|
||||
<Bar
|
||||
dataKey="count"
|
||||
fill="oklch(70% 0.19 55)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar dataKey="count" fill="oklch(70% 0.19 55)" radius={0} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
|
@ -143,9 +139,7 @@ export function DashboardPage() {
|
|||
<section className={styles.panel}>
|
||||
<h2 className={styles.panelLabel}>Top Usernames</h2>
|
||||
<div className={styles.list}>
|
||||
{credentials.top_usernames
|
||||
.slice(0, TOP_CREDS_LIMIT)
|
||||
.map((u) => (
|
||||
{credentials.top_usernames.slice(0, TOP_CREDS_LIMIT).map((u) => (
|
||||
<div key={u.value} className={styles.listRow}>
|
||||
<span className={styles.listLabel}>{u.value}</span>
|
||||
<span className={styles.listValue}>{u.count}</span>
|
||||
|
|
@ -157,9 +151,7 @@ export function DashboardPage() {
|
|||
<section className={styles.panel}>
|
||||
<h2 className={styles.panelLabel}>Top Passwords</h2>
|
||||
<div className={styles.list}>
|
||||
{credentials.top_passwords
|
||||
.slice(0, TOP_CREDS_LIMIT)
|
||||
.map((p) => (
|
||||
{credentials.top_passwords.slice(0, TOP_CREDS_LIMIT).map((p) => (
|
||||
<div key={p.value} className={styles.listRow}>
|
||||
<span className={styles.listLabel}>{p.value}</span>
|
||||
<span className={styles.listValue}>{p.count}</span>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ const PAGE_SIZE = 50
|
|||
|
||||
export function EventsPage() {
|
||||
const [ipFilter, setIpFilter] = useState('')
|
||||
const { data: events, isLoading } = useEvents(
|
||||
PAGE_SIZE,
|
||||
ipFilter || undefined
|
||||
)
|
||||
const { data: events, isLoading } = useEvents(PAGE_SIZE, ipFilter || undefined)
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
|
|
|
|||
|
|
@ -27,24 +27,16 @@ export function IntelPage() {
|
|||
const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_STIX, {
|
||||
responseType: 'text',
|
||||
})
|
||||
downloadBlob(
|
||||
res.data as string,
|
||||
'hive-iocs.stix.json',
|
||||
'application/json'
|
||||
)
|
||||
downloadBlob(res.data as string, 'hive-iocs.stix.json', 'application/json')
|
||||
}
|
||||
|
||||
async function exportBlocklist(format: string) {
|
||||
const res = await apiClient.get(
|
||||
API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST,
|
||||
{ params: { format }, responseType: 'text' }
|
||||
)
|
||||
const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST, {
|
||||
params: { format },
|
||||
responseType: 'text',
|
||||
})
|
||||
const ext = format === 'csv' ? '.csv' : '.txt'
|
||||
downloadBlob(
|
||||
res.data as string,
|
||||
`hive-blocklist${ext}`,
|
||||
'text/plain'
|
||||
)
|
||||
downloadBlob(res.data as string, `hive-blocklist${ext}`, 'text/plain')
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -52,18 +44,12 @@ export function IntelPage() {
|
|||
<header className={styles.heading}>
|
||||
<div className={styles.headingLeft}>
|
||||
<h1 className={styles.title}>Intel</h1>
|
||||
<span className={styles.subtitle}>
|
||||
THREAT INTELLIGENCE PRODUCTS
|
||||
</span>
|
||||
<span className={styles.subtitle}>THREAT INTELLIGENCE PRODUCTS</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.exports}>
|
||||
<span className={styles.exportLabel}>EXPORT</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.exportBtn}
|
||||
onClick={exportSTIX}
|
||||
>
|
||||
<button type="button" className={styles.exportBtn} onClick={exportSTIX}>
|
||||
STIX 2.1
|
||||
</button>
|
||||
{BLOCKLIST_FORMATS.map((fmt) => (
|
||||
|
|
@ -103,12 +89,8 @@ export function IntelPage() {
|
|||
<td>{ioc.confidence}%</td>
|
||||
<td>{ioc.sight_count}</td>
|
||||
<td>{ioc.source}</td>
|
||||
<td>
|
||||
{new Date(ioc.first_seen).toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
{new Date(ioc.last_seen).toLocaleDateString()}
|
||||
</td>
|
||||
<td>{new Date(ioc.first_seen).toLocaleDateString()}</td>
|
||||
<td>{new Date(ioc.last_seen).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -120,25 +102,20 @@ export function IntelPage() {
|
|||
className={styles.pageBtn}
|
||||
disabled={offset === 0}
|
||||
onClick={() =>
|
||||
setOffset(
|
||||
Math.max(0, offset - PAGINATION.DEFAULT_LIMIT)
|
||||
)
|
||||
setOffset(Math.max(0, offset - PAGINATION.DEFAULT_LIMIT))
|
||||
}
|
||||
>
|
||||
◀ PREV
|
||||
</button>
|
||||
<span className={styles.pageInfo}>
|
||||
{offset + 1}–
|
||||
{Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF{' '}
|
||||
{total}
|
||||
{Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF {total}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.pageBtn}
|
||||
disabled={offset + PAGINATION.DEFAULT_LIMIT >= total}
|
||||
onClick={() =>
|
||||
setOffset(offset + PAGINATION.DEFAULT_LIMIT)
|
||||
}
|
||||
onClick={() => setOffset(offset + PAGINATION.DEFAULT_LIMIT)}
|
||||
>
|
||||
NEXT ▶
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ export function MitrePage() {
|
|||
const { data: techniques } = useMitreTechniques()
|
||||
const { data: heatmap } = useMitreHeatmap()
|
||||
|
||||
const countMap = new Map(
|
||||
heatmap?.map((h) => [h.technique_id, h.count])
|
||||
)
|
||||
const countMap = new Map(heatmap?.map((h) => [h.technique_id, h.count]))
|
||||
|
||||
const byTactic = new Map<string, typeof techniques>()
|
||||
for (const t of techniques ?? []) {
|
||||
|
|
@ -51,9 +49,7 @@ export function MitrePage() {
|
|||
<span className={styles.legendLabel}>INTENSITY</span>
|
||||
{HEAT_THRESHOLDS.map((t, i) => (
|
||||
<span key={t} className={styles.legendItem}>
|
||||
<span
|
||||
className={`${styles.legendSwatch} ${styles[`heat${i}`]}`}
|
||||
/>
|
||||
<span className={`${styles.legendSwatch} ${styles[`heat${i}`]}`} />
|
||||
<span className={styles.legendText}>{t}+</span>
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -66,9 +62,7 @@ export function MitrePage() {
|
|||
|
||||
return (
|
||||
<div key={tactic} className={styles.tacticColumn}>
|
||||
<h3 className={styles.tacticLabel}>
|
||||
{tactic.replace(/-/g, ' ')}
|
||||
</h3>
|
||||
<h3 className={styles.tacticLabel}>{tactic.replace(/-/g, ' ')}</h3>
|
||||
<div className={styles.techniques}>
|
||||
{techs.map((t) => {
|
||||
const count = countMap.get(t.id) ?? 0
|
||||
|
|
@ -83,9 +77,7 @@ export function MitrePage() {
|
|||
<span className={styles.techId}>{t.id}</span>
|
||||
<span className={styles.techName}>{t.name}</span>
|
||||
{count > 0 && (
|
||||
<span className={styles.techCount}>
|
||||
{count}
|
||||
</span>
|
||||
<span className={styles.techCount}>{count}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,9 +44,7 @@ export function SessionDetailPage() {
|
|||
← SESSIONS
|
||||
</Link>
|
||||
|
||||
<h1 className={styles.detailTitle}>
|
||||
SESSION {session.id.slice(0, 8)}
|
||||
</h1>
|
||||
<h1 className={styles.detailTitle}>SESSION {session.id.slice(0, 8)}</h1>
|
||||
|
||||
<div className={styles.dossier}>
|
||||
{rows.map((row) => (
|
||||
|
|
|
|||
|
|
@ -52,10 +52,7 @@ export function SessionsPage() {
|
|||
return (
|
||||
<tr key={s.id}>
|
||||
<td>
|
||||
<Link
|
||||
to={`/sessions/${s.id}`}
|
||||
className={styles.link}
|
||||
>
|
||||
<Link to={`/sessions/${s.id}`} className={styles.link}>
|
||||
{s.id.slice(0, 8)}
|
||||
</Link>
|
||||
</td>
|
||||
|
|
@ -67,9 +64,7 @@ export function SessionsPage() {
|
|||
<td>{s.command_count}</td>
|
||||
<td className={styles.threat}>{s.threat_score}</td>
|
||||
<td>{new Date(s.started_at).toLocaleString()}</td>
|
||||
<td>
|
||||
{duration !== null ? `${duration}s` : 'active'}
|
||||
</td>
|
||||
<td>{duration !== null ? `${duration}s` : 'active'}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
|
|
@ -81,15 +76,12 @@ export function SessionsPage() {
|
|||
type="button"
|
||||
className={styles.pageBtn}
|
||||
disabled={offset === 0}
|
||||
onClick={() =>
|
||||
setOffset(Math.max(0, offset - PAGE_SIZE))
|
||||
}
|
||||
onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
|
||||
>
|
||||
◀ PREV
|
||||
</button>
|
||||
<span className={styles.pageInfo}>
|
||||
{offset + 1}–{Math.min(offset + PAGE_SIZE, total)}{' '}
|
||||
OF {total}
|
||||
{offset + 1}–{Math.min(offset + PAGE_SIZE, total)} OF {total}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
type AlertRule,
|
||||
useAlertRules,
|
||||
useChangePassword,
|
||||
useLogout,
|
||||
useDeleteAlertRule,
|
||||
useLogout,
|
||||
useUpdateAlertRule,
|
||||
useUpdateEmail,
|
||||
useUpdateProfile,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,18 @@ const (
|
|||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run())
|
||||
}
|
||||
|
||||
func run() int {
|
||||
client := &http.Client{Timeout: httpDialTO}
|
||||
resp, err := client.Get(healthURL)
|
||||
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 {
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,6 @@ Connects to:
|
|||
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/DictionaryAttack.hpp"
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
|
|
@ -45,20 +40,31 @@ Connects to:
|
|||
#include "src/hash/SHA1Hasher.hpp"
|
||||
#include "src/hash/SHA256Hasher.hpp"
|
||||
#include "src/hash/SHA512Hasher.hpp"
|
||||
#include <boost/program_options.hpp>
|
||||
#include <expected>
|
||||
#include <iostream>
|
||||
#include <print>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
|
||||
auto has = [&](std::string_view token) {
|
||||
return spec.find(token) != std::string::npos;
|
||||
};
|
||||
auto has = [&](std::string_view token) { return spec.find(token) != std::string::npos; };
|
||||
|
||||
if (has("lower")) { result += config::CHARSET_LOWER; }
|
||||
if (has("upper")) { result += config::CHARSET_UPPER; }
|
||||
if (has("digits")) { result += config::CHARSET_DIGITS; }
|
||||
if (has("special")) { result += config::CHARSET_SPECIAL; }
|
||||
if (has("lower")) {
|
||||
result += config::CHARSET_LOWER;
|
||||
}
|
||||
if (has("upper")) {
|
||||
result += config::CHARSET_UPPER;
|
||||
}
|
||||
if (has("digits")) {
|
||||
result += config::CHARSET_DIGITS;
|
||||
}
|
||||
if (has("special")) {
|
||||
result += config::CHARSET_SPECIAL;
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result += config::CHARSET_LOWER;
|
||||
|
|
@ -69,8 +75,7 @@ static std::string build_charset(const std::string& spec) {
|
|||
}
|
||||
|
||||
template <Hasher H>
|
||||
static auto dispatch_attack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError> {
|
||||
static auto dispatch_attack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
|
||||
if (cfg.bruteforce) {
|
||||
return Engine::crack<H, BruteForceAttack>(cfg);
|
||||
}
|
||||
|
|
@ -80,13 +85,17 @@ static auto dispatch_attack(const CrackConfig& 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> {
|
||||
switch (type) {
|
||||
case HashType::MD5: return dispatch_attack<MD5Hasher>(cfg);
|
||||
case HashType::SHA1: return dispatch_attack<SHA1Hasher>(cfg);
|
||||
case HashType::SHA256: return dispatch_attack<SHA256Hasher>(cfg);
|
||||
case HashType::SHA512: return dispatch_attack<SHA512Hasher>(cfg);
|
||||
case HashType::MD5:
|
||||
return dispatch_attack<MD5Hasher>(cfg);
|
||||
case HashType::SHA1:
|
||||
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);
|
||||
}
|
||||
|
|
@ -96,21 +105,34 @@ static std::string json_escape(std::string_view s) {
|
|||
result.reserve(s.size());
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': result += "\\\""; break;
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default: result += c; break;
|
||||
case '"':
|
||||
result += "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
result += "\\\\";
|
||||
break;
|
||||
case '\n':
|
||||
result += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
result += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
result += "\\t";
|
||||
break;
|
||||
default:
|
||||
result += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void write_json_result(const std::string& path,
|
||||
const CrackResult& result) {
|
||||
auto* f = std::fopen(path.c_str(), "w");
|
||||
if (!f) { return; }
|
||||
static void write_json_result(const std::string &path, const CrackResult &result) {
|
||||
auto *f = std::fopen(path.c_str(), "w");
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(f,
|
||||
"{\n"
|
||||
|
|
@ -121,43 +143,39 @@ static void write_json_result(const std::string& path,
|
|||
" \"candidates_tested\": %zu,\n"
|
||||
" \"hashes_per_second\": %.2f\n"
|
||||
"}\n",
|
||||
json_escape(result.plaintext).c_str(),
|
||||
json_escape(result.hash).c_str(),
|
||||
json_escape(result.algorithm).c_str(),
|
||||
result.elapsed_seconds,
|
||||
result.candidates_tested,
|
||||
result.hashes_per_second);
|
||||
json_escape(result.plaintext).c_str(), json_escape(result.hash).c_str(),
|
||||
json_escape(result.algorithm).c_str(), result.elapsed_seconds,
|
||||
result.candidates_tested, result.hashes_per_second);
|
||||
|
||||
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");
|
||||
desc.add_options()
|
||||
("help,h", "Show help message")
|
||||
("hash", po::value<std::string>(), "Target hash to crack")
|
||||
("type", po::value<std::string>()->default_value("auto"),
|
||||
"Hash type: md5, sha1, sha256, sha512, auto")
|
||||
("wordlist,w", po::value<std::string>(), "Path to wordlist file")
|
||||
("bruteforce,b", "Use brute-force attack mode")
|
||||
("charset", po::value<std::string>()->default_value("lower,digits"),
|
||||
"Character sets: lower,upper,digits,special")
|
||||
("max-length", po::value<std::size_t>()->default_value(
|
||||
config::DEFAULT_MAX_BRUTE_LENGTH), "Max password length for brute-force")
|
||||
("rules,r", "Apply mutation rules to dictionary words")
|
||||
("chain-rules", "Chain mutation rules in combination")
|
||||
("salt", po::value<std::string>(), "Salt value to prepend/append")
|
||||
("salt-position", po::value<std::string>()->default_value("prepend"),
|
||||
"Salt position: prepend or append")
|
||||
("threads,t", po::value<unsigned>()->default_value(
|
||||
config::DEFAULT_THREAD_COUNT), "Thread count (0 = auto)")
|
||||
("output,o", po::value<std::string>(), "Write JSON result to file");
|
||||
desc.add_options()("help,h", "Show help message")("hash", po::value<std::string>(),
|
||||
"Target hash to crack")(
|
||||
"type", po::value<std::string>()->default_value("auto"),
|
||||
"Hash type: md5, sha1, sha256, sha512, auto")("wordlist,w", po::value<std::string>(),
|
||||
"Path to wordlist file")(
|
||||
"bruteforce,b", "Use brute-force attack mode")(
|
||||
"charset", po::value<std::string>()->default_value("lower,digits"),
|
||||
"Character sets: lower,upper,digits,special")(
|
||||
"max-length", po::value<std::size_t>()->default_value(config::DEFAULT_MAX_BRUTE_LENGTH),
|
||||
"Max password length for brute-force")("rules,r",
|
||||
"Apply mutation rules to dictionary words")(
|
||||
"chain-rules", "Chain mutation rules in combination")("salt", po::value<std::string>(),
|
||||
"Salt value to prepend/append")(
|
||||
"salt-position", po::value<std::string>()->default_value("prepend"),
|
||||
"Salt position: prepend or append")(
|
||||
"threads,t", po::value<unsigned>()->default_value(config::DEFAULT_THREAD_COUNT),
|
||||
"Thread count (0 = auto)")("output,o", po::value<std::string>(),
|
||||
"Write JSON result to file");
|
||||
|
||||
po::variables_map vm;
|
||||
try {
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
} catch (const po::error& e) {
|
||||
} catch (const po::error &e) {
|
||||
std::println(stderr, "Error: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -198,8 +216,7 @@ int main(int argc, char* argv[]) {
|
|||
if (cfg.hash_type == "auto") {
|
||||
auto detected = HashDetector::detect(cfg.target_hash);
|
||||
if (!detected.has_value()) {
|
||||
std::println(stderr, "Error: {}",
|
||||
crack_error_message(detected.error()));
|
||||
std::println(stderr, "Error: {}", crack_error_message(detected.error()));
|
||||
return 1;
|
||||
}
|
||||
hash_type = *detected;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ Connects to:
|
|||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size,
|
||||
std::size_t max_length) {
|
||||
std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size, std::size_t max_length) {
|
||||
std::size_t total = 0;
|
||||
std::size_t power = 1;
|
||||
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;
|
||||
}
|
||||
|
||||
BruteForceAttack::BruteForceAttack(std::string_view charset,
|
||||
std::size_t max_length,
|
||||
unsigned thread_index,
|
||||
unsigned total_threads)
|
||||
BruteForceAttack::BruteForceAttack(std::string_view charset, std::size_t max_length,
|
||||
unsigned thread_index, unsigned total_threads)
|
||||
: charset_(charset), max_length_(max_length),
|
||||
total_keyspace_(compute_keyspace(charset.size(), max_length)) {
|
||||
std::size_t per_thread = total_keyspace_ / total_threads;
|
||||
std::size_t remainder = total_keyspace_ % total_threads;
|
||||
|
||||
start_index_ = thread_index * per_thread
|
||||
+ std::min(static_cast<std::size_t>(thread_index), remainder);
|
||||
start_index_ =
|
||||
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);
|
||||
end_index_ = start_index_ + my_count;
|
||||
current_index_ = start_index_;
|
||||
|
|
@ -83,5 +80,9 @@ std::expected<std::string, AttackComplete> BruteForceAttack::next() {
|
|||
return index_to_candidate(current_index_++);
|
||||
}
|
||||
|
||||
std::size_t BruteForceAttack::total() const { return total_keyspace_; }
|
||||
std::size_t BruteForceAttack::progress() const { return current_index_ - start_index_; }
|
||||
std::size_t BruteForceAttack::total() const {
|
||||
return total_keyspace_;
|
||||
}
|
||||
std::size_t BruteForceAttack::progress() const {
|
||||
return current_index_ - start_index_;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,22 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class BruteForceAttack {
|
||||
public:
|
||||
BruteForceAttack(std::string_view charset, std::size_t max_length,
|
||||
unsigned thread_index, unsigned total_threads);
|
||||
public:
|
||||
BruteForceAttack(std::string_view charset, std::size_t max_length, unsigned thread_index,
|
||||
unsigned total_threads);
|
||||
|
||||
std::expected<std::string, AttackComplete> next();
|
||||
std::size_t total() const;
|
||||
std::size_t progress() const;
|
||||
|
||||
private:
|
||||
private:
|
||||
std::string charset_;
|
||||
std::size_t max_length_;
|
||||
std::size_t total_keyspace_;
|
||||
|
|
@ -36,6 +36,5 @@ private:
|
|||
std::size_t current_index_;
|
||||
|
||||
std::string index_to_candidate(std::size_t index) const;
|
||||
static std::size_t compute_keyspace(std::size_t charset_size,
|
||||
std::size_t max_length);
|
||||
static std::size_t compute_keyspace(std::size_t charset_size, std::size_t max_length);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ Connects to:
|
|||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
static std::size_t count_lines_in_range(const char* data,
|
||||
std::size_t start,
|
||||
std::size_t end) {
|
||||
static std::size_t count_lines_in_range(const char *data, std::size_t start, std::size_t end) {
|
||||
std::size_t count = 0;
|
||||
for (std::size_t i = start; i < end; ++i) {
|
||||
if (data[i] == '\n') {
|
||||
|
|
@ -39,23 +37,21 @@ static std::size_t count_lines_in_range(const char* data,
|
|||
return count;
|
||||
}
|
||||
|
||||
static std::size_t find_next_newline(const char* data,
|
||||
std::size_t pos,
|
||||
std::size_t size) {
|
||||
static std::size_t find_next_newline(const char *data, std::size_t pos, std::size_t size) {
|
||||
while (pos < size && data[pos] != '\n') {
|
||||
++pos;
|
||||
}
|
||||
return pos < size ? pos + 1 : size;
|
||||
}
|
||||
|
||||
std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
|
||||
std::string_view path, unsigned thread_index, unsigned total_threads) {
|
||||
std::expected<DictionaryAttack, CrackError>
|
||||
DictionaryAttack::create(std::string_view path, unsigned thread_index, unsigned total_threads) {
|
||||
auto file = MappedFile::open(path);
|
||||
if (!file.has_value()) {
|
||||
return std::unexpected(file.error());
|
||||
}
|
||||
|
||||
auto* data = file->data();
|
||||
auto *data = file->data();
|
||||
auto file_size = 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 remainder = total_lines % total_threads;
|
||||
|
||||
std::size_t my_start_line = thread_index * lines_per_thread
|
||||
+ std::min(static_cast<std::size_t>(thread_index), remainder);
|
||||
std::size_t my_line_count = lines_per_thread
|
||||
+ (thread_index < remainder ? 1 : 0);
|
||||
std::size_t my_start_line = thread_index * lines_per_thread +
|
||||
std::min(static_cast<std::size_t>(thread_index), remainder);
|
||||
std::size_t my_line_count = lines_per_thread + (thread_index < remainder ? 1 : 0);
|
||||
|
||||
std::size_t start_offset = 0;
|
||||
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{});
|
||||
}
|
||||
|
||||
std::size_t DictionaryAttack::total() const { return total_words_; }
|
||||
std::size_t DictionaryAttack::progress() const { return words_read_; }
|
||||
std::size_t DictionaryAttack::total() const {
|
||||
return total_words_;
|
||||
}
|
||||
std::size_t DictionaryAttack::progress() const {
|
||||
return words_read_;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,23 +14,23 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include "src/io/MappedFile.hpp"
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include "src/io/MappedFile.hpp"
|
||||
|
||||
class DictionaryAttack {
|
||||
public:
|
||||
static std::expected<DictionaryAttack, CrackError> create(
|
||||
std::string_view path, unsigned thread_index, unsigned total_threads);
|
||||
public:
|
||||
static std::expected<DictionaryAttack, CrackError>
|
||||
create(std::string_view path, unsigned thread_index, unsigned total_threads);
|
||||
|
||||
std::expected<std::string, AttackComplete> next();
|
||||
std::size_t total() const;
|
||||
std::size_t progress() const;
|
||||
|
||||
private:
|
||||
private:
|
||||
DictionaryAttack() = default;
|
||||
|
||||
MappedFile file_;
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ Connects to:
|
|||
RuleAttack::RuleAttack(DictionaryAttack dict, bool chain_rules)
|
||||
: dict_(std::move(dict)), chain_rules_(chain_rules) {}
|
||||
|
||||
std::expected<RuleAttack, CrackError> RuleAttack::create(
|
||||
std::string_view path, bool chain_rules,
|
||||
unsigned thread_index, unsigned total_threads) {
|
||||
std::expected<RuleAttack, CrackError> RuleAttack::create(std::string_view path, bool chain_rules,
|
||||
unsigned thread_index,
|
||||
unsigned total_threads) {
|
||||
auto dict = DictionaryAttack::create(path, thread_index, total_threads);
|
||||
if (!dict.has_value()) {
|
||||
return std::unexpected(dict.error());
|
||||
|
|
@ -49,14 +49,14 @@ bool RuleAttack::load_next_word() {
|
|||
mutations_.clear();
|
||||
mutations_.push_back(*word);
|
||||
|
||||
for (auto&& m : RuleSet::apply_all(*word)) {
|
||||
for (auto &&m : RuleSet::apply_all(*word)) {
|
||||
mutations_.push_back(std::move(m));
|
||||
}
|
||||
|
||||
if (chain_rules_) {
|
||||
std::vector<std::string> base(mutations_.begin() + 1, mutations_.end());
|
||||
for (const auto& b : base) {
|
||||
for (auto&& m : RuleSet::apply_all(b)) {
|
||||
for (const auto &b : base) {
|
||||
for (auto &&m : RuleSet::apply_all(b)) {
|
||||
mutations_.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
|
|
@ -77,5 +77,9 @@ std::expected<std::string, AttackComplete> RuleAttack::next() {
|
|||
return std::move(mutations_[mutation_index_++]);
|
||||
}
|
||||
|
||||
std::size_t RuleAttack::total() const { return dict_.total(); }
|
||||
std::size_t RuleAttack::progress() const { return candidates_yielded_; }
|
||||
std::size_t RuleAttack::total() const {
|
||||
return dict_.total();
|
||||
}
|
||||
std::size_t RuleAttack::progress() const {
|
||||
return candidates_yielded_;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,25 +17,24 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class RuleAttack {
|
||||
public:
|
||||
static std::expected<RuleAttack, CrackError> create(
|
||||
std::string_view path, bool chain_rules,
|
||||
unsigned thread_index, unsigned total_threads);
|
||||
public:
|
||||
static std::expected<RuleAttack, CrackError>
|
||||
create(std::string_view path, bool chain_rules, unsigned thread_index, unsigned total_threads);
|
||||
|
||||
std::expected<std::string, AttackComplete> next();
|
||||
std::size_t total() const;
|
||||
std::size_t progress() const;
|
||||
|
||||
private:
|
||||
private:
|
||||
RuleAttack(DictionaryAttack dict, bool chain_rules);
|
||||
|
||||
DictionaryAttack dict_;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ constexpr std::string_view CYAN = "\033[36m";
|
|||
constexpr std::string_view BOLD = "\033[1m";
|
||||
constexpr std::string_view DIM = "\033[2m";
|
||||
|
||||
}
|
||||
} // namespace color
|
||||
|
||||
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_RIGHT = "\u258C";
|
||||
|
||||
}
|
||||
} // namespace box
|
||||
|
||||
namespace symbol {
|
||||
|
||||
|
|
@ -99,9 +99,9 @@ constexpr std::string_view TRIANGLE_UP = "\u25B2";
|
|||
constexpr std::string_view STAR = "\u2726";
|
||||
constexpr std::string_view DIVIDER_CHAR = "\u2501";
|
||||
|
||||
}
|
||||
} // namespace symbol
|
||||
|
||||
}
|
||||
} // namespace config
|
||||
|
||||
struct CrackConfig {
|
||||
std::string target_hash;
|
||||
|
|
|
|||
|
|
@ -50,12 +50,18 @@ enum class CrackError {
|
|||
|
||||
constexpr std::string_view crack_error_message(CrackError e) {
|
||||
switch (e) {
|
||||
case CrackError::FileNotFound: return "File not found";
|
||||
case CrackError::InvalidHash: return "Invalid hash format";
|
||||
case CrackError::UnsupportedAlgorithm: return "Unsupported hash algorithm";
|
||||
case CrackError::OpenSSLError: return "OpenSSL internal error";
|
||||
case CrackError::InvalidConfig: return "Invalid configuration";
|
||||
case CrackError::Exhausted: return "All candidates exhausted";
|
||||
case CrackError::FileNotFound:
|
||||
return "File not found";
|
||||
case CrackError::InvalidHash:
|
||||
return "Invalid hash format";
|
||||
case CrackError::UnsupportedAlgorithm:
|
||||
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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,6 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
|
|
@ -43,26 +38,32 @@ Connects to:
|
|||
#include "src/core/Concepts.hpp"
|
||||
#include "src/display/Progress.hpp"
|
||||
#include "src/threading/ThreadPool.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class Engine {
|
||||
public:
|
||||
public:
|
||||
template <Hasher H, AttackStrategy A>
|
||||
static auto crack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError>;
|
||||
static auto crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError>;
|
||||
};
|
||||
|
||||
template <Hasher H, AttackStrategy A>
|
||||
auto Engine::crack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError> {
|
||||
unsigned thread_count = cfg.thread_count > 0
|
||||
? cfg.thread_count
|
||||
: std::thread::hardware_concurrency();
|
||||
auto Engine::crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
|
||||
unsigned thread_count =
|
||||
cfg.thread_count > 0 ? cfg.thread_count : std::thread::hardware_concurrency();
|
||||
|
||||
ThreadPool pool(thread_count);
|
||||
|
||||
auto attack_name = [&]() -> std::string_view {
|
||||
if (cfg.bruteforce) { return "Brute Force"; }
|
||||
if (cfg.use_rules) { return "Rules"; }
|
||||
if (cfg.bruteforce) {
|
||||
return "Brute Force";
|
||||
}
|
||||
if (cfg.use_rules) {
|
||||
return "Rules";
|
||||
}
|
||||
return "Dictionary";
|
||||
}();
|
||||
|
||||
|
|
@ -72,17 +73,20 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
return probe.total();
|
||||
} else if constexpr (std::same_as<A, RuleAttack>) {
|
||||
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
|
||||
if (!probe) { return 0; }
|
||||
if (!probe) {
|
||||
return 0;
|
||||
}
|
||||
return probe->total() * 2005;
|
||||
} else {
|
||||
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
|
||||
if (!probe) { return 0; }
|
||||
if (!probe) {
|
||||
return 0;
|
||||
}
|
||||
return probe->total();
|
||||
}
|
||||
}();
|
||||
|
||||
Progress progress(H::name(), attack_name, thread_count,
|
||||
total_estimate, pool.state().found,
|
||||
Progress progress(H::name(), attack_name, thread_count, total_estimate, pool.state().found,
|
||||
pool.state().tested_count);
|
||||
|
||||
if (Progress::is_tty()) {
|
||||
|
|
@ -97,16 +101,14 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
std::jthread display_thread;
|
||||
if (Progress::is_tty()) {
|
||||
display_thread = std::jthread([&](std::stop_token st) {
|
||||
while (!st.stop_requested() &&
|
||||
!pool.state().found.load(std::memory_order_relaxed)) {
|
||||
while (!st.stop_requested() && !pool.state().found.load(std::memory_order_relaxed)) {
|
||||
progress.update();
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
|
||||
std::this_thread::sleep_for(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;
|
||||
|
||||
auto create_attack = [&]() {
|
||||
|
|
@ -114,20 +116,23 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
return std::expected<BruteForceAttack, CrackError>(
|
||||
BruteForceAttack(cfg.charset, cfg.max_length, tid, total));
|
||||
} else if constexpr (std::same_as<A, RuleAttack>) {
|
||||
return RuleAttack::create(
|
||||
cfg.wordlist_path, cfg.chain_rules, tid, total);
|
||||
return RuleAttack::create(cfg.wordlist_path, cfg.chain_rules, tid, total);
|
||||
} else {
|
||||
return DictionaryAttack::create(cfg.wordlist_path, tid, total);
|
||||
}
|
||||
};
|
||||
|
||||
auto attack = create_attack();
|
||||
if (!attack.has_value()) { return; }
|
||||
if (!attack.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t local_count = 0;
|
||||
while (!state.found.load(std::memory_order_relaxed)) {
|
||||
auto candidate = attack->next();
|
||||
if (!candidate.has_value()) { break; }
|
||||
if (!candidate.has_value()) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string to_hash = *candidate;
|
||||
if (!cfg.salt.empty()) {
|
||||
|
|
@ -161,18 +166,16 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
auto end = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(end - start).count();
|
||||
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()) {
|
||||
CrackResult result{
|
||||
.plaintext = *state.result,
|
||||
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
|
||||
CrackResult result{.plaintext = *state.result,
|
||||
.hash = cfg.target_hash,
|
||||
.algorithm = std::string(H::name()),
|
||||
.elapsed_seconds = elapsed,
|
||||
.candidates_tested = tested,
|
||||
.hashes_per_second = speed
|
||||
};
|
||||
.hashes_per_second = speed};
|
||||
|
||||
progress.print_cracked(result);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -35,13 +35,11 @@ Connects to:
|
|||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
|
||||
unsigned thread_count, std::size_t total_candidates,
|
||||
const std::atomic<bool>& found,
|
||||
const std::atomic<std::size_t>& tested)
|
||||
: algorithm_(algorithm), attack_mode_(attack_mode),
|
||||
thread_count_(thread_count), total_(total_candidates),
|
||||
found_(found), tested_(tested),
|
||||
Progress::Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
|
||||
std::size_t total_candidates, const std::atomic<bool> &found,
|
||||
const std::atomic<std::size_t> &tested)
|
||||
: algorithm_(algorithm), attack_mode_(attack_mode), thread_count_(thread_count),
|
||||
total_(total_candidates), found_(found), tested_(tested),
|
||||
start_time_(std::chrono::steady_clock::now()) {}
|
||||
|
||||
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));
|
||||
if (filled > width) { filled = width; }
|
||||
if (filled > width) {
|
||||
filled = width;
|
||||
}
|
||||
|
||||
std::string bar;
|
||||
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 {
|
||||
if (!is_tty()) { return; }
|
||||
if (!is_tty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto w = terminal_width();
|
||||
auto inner_width = (w > 6) ? w - 6 : 40;
|
||||
|
|
@ -111,42 +113,38 @@ void Progress::print_banner() const {
|
|||
bot_border += config::box::HORIZONTAL;
|
||||
}
|
||||
|
||||
auto line1 = std::format(" {} {} v{}",
|
||||
config::APP_NAME, config::box::VERTICAL, config::VERSION);
|
||||
auto line2 = std::format(" {} {} {} {} {} threads",
|
||||
config::box::VERTICAL, algorithm_, config::box::VERTICAL,
|
||||
attack_mode_, thread_count_);
|
||||
auto line1 =
|
||||
std::format(" {} {} v{}", config::APP_NAME, config::box::VERTICAL, config::VERSION);
|
||||
auto line2 = std::format(" {} {} {} {} {} threads", config::box::VERTICAL, algorithm_,
|
||||
config::box::VERTICAL, attack_mode_, thread_count_);
|
||||
|
||||
std::println("{}{}{}{}{}",
|
||||
config::color::CYAN, config::box::TOP_LEFT,
|
||||
top_border, config::box::TOP_RIGHT, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
|
||||
config::box::VERTICAL, line1, inner_width - 1,
|
||||
config::box::VERTICAL, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
|
||||
config::box::VERTICAL, line2, inner_width - 1,
|
||||
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("{}{}{}{}{}", config::color::CYAN, config::box::TOP_LEFT, top_border,
|
||||
config::box::TOP_RIGHT, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line1,
|
||||
inner_width - 1, config::box::VERTICAL, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line2,
|
||||
inner_width - 1, 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("");
|
||||
}
|
||||
|
||||
void Progress::update() {
|
||||
if (!is_tty()) { return; }
|
||||
if (!is_tty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time_).count();
|
||||
auto tested_val = tested_.load(std::memory_order_relaxed);
|
||||
|
||||
double fraction = (total_ > 0)
|
||||
? static_cast<double>(tested_val) / static_cast<double>(total_)
|
||||
: 0.0;
|
||||
if (fraction > 1.0) { fraction = 1.0; }
|
||||
double fraction =
|
||||
(total_ > 0) ? static_cast<double>(tested_val) / static_cast<double>(total_) : 0.0;
|
||||
if (fraction > 1.0) {
|
||||
fraction = 1.0;
|
||||
}
|
||||
|
||||
double speed = (elapsed > 0.0)
|
||||
? static_cast<double>(tested_val) / elapsed
|
||||
: 0.0;
|
||||
double speed = (elapsed > 0.0) ? static_cast<double>(tested_val) / elapsed : 0.0;
|
||||
|
||||
double eta = (speed > 0.0 && total_ > tested_val)
|
||||
? static_cast<double>(total_ - tested_val) / speed
|
||||
|
|
@ -157,22 +155,16 @@ void Progress::update() {
|
|||
|
||||
std::print("\033[3A");
|
||||
|
||||
std::println(" {}{} {:.1f}%{}",
|
||||
config::color::YELLOW, render_bar(fraction, bar_width),
|
||||
std::println(" {}{} {:.1f}%{}", config::color::YELLOW, render_bar(fraction, bar_width),
|
||||
fraction * 100.0, config::color::RESET);
|
||||
std::println(" {} {} {} {} {} {} {} ~{}{}",
|
||||
config::symbol::DIAMOND, config::color::CYAN,
|
||||
format_speed(speed), config::color::RESET,
|
||||
config::symbol::TIMER, config::color::CYAN,
|
||||
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);
|
||||
std::println(" {} {} {} {} {} {} {} ~{}{}", config::symbol::DIAMOND, config::color::CYAN,
|
||||
format_speed(speed), config::color::RESET, config::symbol::TIMER,
|
||||
config::color::CYAN, 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()) {
|
||||
std::println("{}", result.plaintext);
|
||||
return;
|
||||
|
|
@ -180,22 +172,17 @@ void Progress::print_cracked(const CrackResult& result) const {
|
|||
|
||||
std::print("\033[3A\033[J");
|
||||
|
||||
std::println(" {}{} CRACKED {}{}{}",
|
||||
config::color::GREEN, config::symbol::CHECK,
|
||||
std::println(" {}{} CRACKED {}{}{}", config::color::GREEN, config::symbol::CHECK,
|
||||
std::string(30, '-'), config::color::RESET, "");
|
||||
std::println(" {}Password: {}{}{}",
|
||||
config::color::BOLD, config::color::GREEN,
|
||||
std::println(" {}Password: {}{}{}", config::color::BOLD, config::color::GREEN,
|
||||
result.plaintext, config::color::RESET);
|
||||
std::println(" Hash: {}", result.hash);
|
||||
std::println(" Algorithm: {}", result.algorithm);
|
||||
std::println(" Time: {} {} {}",
|
||||
format_time(result.elapsed_seconds),
|
||||
config::box::VERTICAL,
|
||||
format_speed(result.hashes_per_second));
|
||||
std::println(" Time: {} {} {}", format_time(result.elapsed_seconds),
|
||||
config::box::VERTICAL, format_speed(result.hashes_per_second));
|
||||
}
|
||||
|
||||
void Progress::print_exhausted(std::string_view hash,
|
||||
std::string_view algorithm) const {
|
||||
void Progress::print_exhausted(std::string_view hash, std::string_view algorithm) const {
|
||||
if (!is_tty()) {
|
||||
std::println("NOT FOUND");
|
||||
return;
|
||||
|
|
@ -207,11 +194,10 @@ void Progress::print_exhausted(std::string_view hash,
|
|||
|
||||
std::print("\033[3A\033[J");
|
||||
|
||||
std::println(" {}{} EXHAUSTED {}{}{}",
|
||||
config::color::RED, config::symbol::CROSS,
|
||||
std::println(" {}{} EXHAUSTED {}{}{}", config::color::RED, config::symbol::CROSS,
|
||||
std::string(30, '-'), config::color::RESET, "");
|
||||
std::println(" Hash: {}", hash);
|
||||
std::println(" Algorithm: {}", algorithm);
|
||||
std::println(" Tested: {} candidates in {}",
|
||||
format_count(tested_val), format_time(elapsed));
|
||||
std::println(" Tested: {} candidates in {}", format_count(tested_val),
|
||||
format_time(elapsed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,25 +21,24 @@ Connects to:
|
|||
struct CrackResult;
|
||||
|
||||
class Progress {
|
||||
public:
|
||||
Progress(std::string_view algorithm, std::string_view attack_mode,
|
||||
unsigned thread_count, std::size_t total_candidates,
|
||||
const std::atomic<bool>& found,
|
||||
const std::atomic<std::size_t>& tested);
|
||||
public:
|
||||
Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
|
||||
std::size_t total_candidates, const std::atomic<bool> &found,
|
||||
const std::atomic<std::size_t> &tested);
|
||||
|
||||
void print_banner() const;
|
||||
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;
|
||||
static bool is_tty();
|
||||
|
||||
private:
|
||||
private:
|
||||
std::string algorithm_;
|
||||
std::string attack_mode_;
|
||||
unsigned thread_count_;
|
||||
std::size_t total_;
|
||||
const std::atomic<bool>& found_;
|
||||
const std::atomic<std::size_t>& tested_;
|
||||
const std::atomic<bool> &found_;
|
||||
const std::atomic<std::size_t> &tested_;
|
||||
|
||||
std::chrono::steady_clock::time_point start_time_;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,56 +42,53 @@ Connects to:
|
|||
namespace evp {
|
||||
|
||||
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::size_t hex_length = 32;
|
||||
};
|
||||
|
||||
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::size_t hex_length = 40;
|
||||
};
|
||||
|
||||
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::size_t hex_length = 64;
|
||||
};
|
||||
|
||||
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::size_t hex_length = 128;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace evp
|
||||
|
||||
inline constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] {
|
||||
std::array<std::array<char, 2>, 256> t{};
|
||||
constexpr std::array<char, 17> digits = {
|
||||
'0','1','2','3','4','5','6','7',
|
||||
'8','9','a','b','c','d','e','f','\0'};
|
||||
constexpr std::array<char, 17> digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
|
||||
'9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
|
||||
for (std::size_t i = 0; i < 256; ++i) {
|
||||
t.at(i) = {digits.at(i >> 4), digits.at(i & 0xF)};
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
|
||||
template <typename Tag>
|
||||
class EVPHasher {
|
||||
public:
|
||||
template <typename Tag> class EVPHasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(EVP_MD_CTX_new(),
|
||||
EVP_MD_CTX_free);
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
|
||||
if (!ctx
|
||||
|| !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr)
|
||||
|| !EVP_DigestUpdate(ctx.get(), input.data(), input.size())
|
||||
|| !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
|
||||
if (!ctx || !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr) ||
|
||||
!EVP_DigestUpdate(ctx.get(), input.data(), input.size()) ||
|
||||
!EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ Connects to:
|
|||
|
||||
std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash) {
|
||||
auto is_hex = [](char c) {
|
||||
return (c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F');
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
};
|
||||
|
||||
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()) {
|
||||
case config::MD5_HEX_LENGTH: return HashType::MD5;
|
||||
case config::SHA1_HEX_LENGTH: return HashType::SHA1;
|
||||
case config::SHA256_HEX_LENGTH: return HashType::SHA256;
|
||||
case config::SHA512_HEX_LENGTH: return HashType::SHA512;
|
||||
default: return std::unexpected(CrackError::InvalidHash);
|
||||
case config::MD5_HEX_LENGTH:
|
||||
return HashType::MD5;
|
||||
case config::SHA1_HEX_LENGTH:
|
||||
return HashType::SHA1;
|
||||
case config::SHA256_HEX_LENGTH:
|
||||
return HashType::SHA256;
|
||||
case config::SHA512_HEX_LENGTH:
|
||||
return HashType::SHA512;
|
||||
default:
|
||||
return std::unexpected(CrackError::InvalidHash);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
enum class HashType { MD5, SHA1, SHA256, SHA512 };
|
||||
|
||||
class HashDetector {
|
||||
public:
|
||||
public:
|
||||
static std::expected<HashType, CrackError> detect(std::string_view hash);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,15 +47,15 @@ std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
|
|||
return std::unexpected(CrackError::InvalidConfig);
|
||||
}
|
||||
|
||||
auto* mapped = static_cast<const char*>(
|
||||
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
auto *mapped =
|
||||
static_cast<const char *>(mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
|
||||
if (mapped == MAP_FAILED) {
|
||||
::close(fd);
|
||||
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;
|
||||
mf.data_ = mapped;
|
||||
|
|
@ -66,23 +66,23 @@ std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
|
|||
|
||||
MappedFile::~MappedFile() {
|
||||
if (data_ && data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(data_), size_);
|
||||
munmap(const_cast<char *>(data_), size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
}
|
||||
}
|
||||
|
||||
MappedFile::MappedFile(MappedFile&& other) noexcept
|
||||
MappedFile::MappedFile(MappedFile &&other) noexcept
|
||||
: data_(other.data_), size_(other.size_), fd_(other.fd_) {
|
||||
other.data_ = nullptr;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
MappedFile& MappedFile::operator=(MappedFile&& other) noexcept {
|
||||
MappedFile &MappedFile::operator=(MappedFile &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (data_ && data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(data_), size_);
|
||||
munmap(const_cast<char *>(data_), size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
|
|
|
|||
|
|
@ -12,28 +12,28 @@ Connects to:
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class MappedFile {
|
||||
public:
|
||||
public:
|
||||
MappedFile() = default;
|
||||
static std::expected<MappedFile, CrackError> open(std::string_view path);
|
||||
|
||||
~MappedFile();
|
||||
MappedFile(MappedFile&& other) noexcept;
|
||||
MappedFile& operator=(MappedFile&& other) noexcept;
|
||||
MappedFile(MappedFile &&other) noexcept;
|
||||
MappedFile &operator=(MappedFile &&other) noexcept;
|
||||
|
||||
MappedFile(const MappedFile&) = delete;
|
||||
MappedFile& operator=(const MappedFile&) = delete;
|
||||
MappedFile(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_; }
|
||||
|
||||
private:
|
||||
const char* data_ = nullptr;
|
||||
private:
|
||||
const char *data_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
int fd_ = -1;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ Connects to:
|
|||
#include <ranges>
|
||||
#include <utility>
|
||||
|
||||
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {{
|
||||
{'a', '@'}, {'e', '3'}, {'i', '1'},
|
||||
{'o', '0'}, {'s', '$'}, {'t', '7'}
|
||||
}};
|
||||
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {
|
||||
{{'a', '@'}, {'e', '3'}, {'i', '1'}, {'o', '0'}, {'s', '$'}, {'t', '7'}}};
|
||||
|
||||
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);
|
||||
result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0])));
|
||||
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::string result(word);
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::toupper(c));
|
||||
});
|
||||
std::ranges::transform(result, result.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::leet_speak(std::string_view 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)));
|
||||
for (auto [from, to] : LEET_MAP) {
|
||||
if (lower == from) { c = to; break; }
|
||||
if (lower == from) {
|
||||
c = to;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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::string result(word);
|
||||
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));
|
||||
});
|
||||
co_yield std::move(result);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Connects to:
|
|||
#include <string_view>
|
||||
|
||||
class RuleSet {
|
||||
public:
|
||||
public:
|
||||
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> leet_speak(std::string_view word);
|
||||
|
|
|
|||
|
|
@ -27,18 +27,17 @@ void SharedState::set_result(std::string plaintext) {
|
|||
}
|
||||
|
||||
ThreadPool::ThreadPool(unsigned thread_count)
|
||||
: thread_count_(thread_count > 0 ? thread_count
|
||||
: std::thread::hardware_concurrency()) {}
|
||||
: thread_count_(thread_count > 0 ? thread_count : std::thread::hardware_concurrency()) {}
|
||||
|
||||
void ThreadPool::run(WorkFn work) {
|
||||
std::vector<std::jthread> threads;
|
||||
threads.reserve(thread_count_);
|
||||
|
||||
for (unsigned i = 0; i < thread_count_; ++i) {
|
||||
threads.emplace_back([this, &work, i] {
|
||||
work(i, thread_count_, state_);
|
||||
});
|
||||
threads.emplace_back([this, &work, i] { work(i, thread_count_, state_); });
|
||||
}
|
||||
}
|
||||
|
||||
SharedState& ThreadPool::state() { return state_; }
|
||||
SharedState &ThreadPool::state() {
|
||||
return state_;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,15 +43,15 @@ struct SharedState {
|
|||
};
|
||||
|
||||
class ThreadPool {
|
||||
public:
|
||||
using WorkFn = std::function<void(unsigned thread_id, unsigned total, SharedState&)>;
|
||||
public:
|
||||
using WorkFn = std::function<void(unsigned thread_id, unsigned total, SharedState &)>;
|
||||
|
||||
explicit ThreadPool(unsigned thread_count);
|
||||
|
||||
void run(WorkFn work);
|
||||
SharedState& state();
|
||||
SharedState &state();
|
||||
|
||||
private:
|
||||
private:
|
||||
unsigned thread_count_;
|
||||
SharedState state_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ Connects to:
|
|||
attack/BruteForceAttack.hpp - BruteForceAttack tested
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -42,8 +42,12 @@ TEST(BruteForceAttackTest, PartitionsKeyspace) {
|
|||
BruteForceAttack p0("ab", 2, 0, 2);
|
||||
BruteForceAttack p1("ab", 2, 1, 2);
|
||||
std::set<std::string> all;
|
||||
while (auto c = p0.next()) { all.insert(*c); }
|
||||
while (auto c = p1.next()) { all.insert(*c); }
|
||||
while (auto c = p0.next()) {
|
||||
all.insert(*c);
|
||||
}
|
||||
while (auto c = p1.next()) {
|
||||
all.insert(*c);
|
||||
}
|
||||
EXPECT_EQ(all.size(), 6);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ Connects to:
|
|||
tests/data/small_wordlist.txt - fixture wordlist
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
|
||||
TEST(DictionaryAttackTest, ReadsAllWords) {
|
||||
|
|
@ -38,8 +38,12 @@ TEST(DictionaryAttackTest, PartitionsAcrossThreads) {
|
|||
ASSERT_TRUE(p1.has_value());
|
||||
|
||||
std::size_t count0 = 0, count1 = 0;
|
||||
while (p0->next()) { ++count0; }
|
||||
while (p1->next()) { ++count1; }
|
||||
while (p0->next()) {
|
||||
++count0;
|
||||
}
|
||||
while (p1->next()) {
|
||||
++count1;
|
||||
}
|
||||
EXPECT_EQ(count0 + count1, 10);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@ Connects to:
|
|||
tests/data/small_wordlist.txt - fixture wordlist
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/core/Engine.hpp"
|
||||
#include "src/hash/SHA256Hasher.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(EngineTest, CracksSHA256WithDictionary) {
|
||||
CrackConfig cfg;
|
||||
cfg.target_hash =
|
||||
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
|
||||
cfg.target_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
|
||||
cfg.wordlist_path = "tests/data/small_wordlist.txt";
|
||||
cfg.thread_count = 2;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ Connects to:
|
|||
core/Concepts.hpp - CrackError enum for error assertions
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/hash/HashDetector.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(HashDetectorTest, DetectsMD5) {
|
||||
auto result = HashDetector::detect("d41d8cd98f00b204e9800998ecf8427e");
|
||||
|
|
@ -29,8 +29,8 @@ TEST(HashDetectorTest, DetectsSHA1) {
|
|||
}
|
||||
|
||||
TEST(HashDetectorTest, DetectsSHA256) {
|
||||
auto result = HashDetector::detect(
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
auto result =
|
||||
HashDetector::detect("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, HashType::SHA256);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,15 @@ Connects to:
|
|||
hash/EVPHasher.hpp - underlying implementation
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/hash/MD5Hasher.hpp"
|
||||
#include "src/hash/SHA1Hasher.hpp"
|
||||
#include "src/hash/SHA256Hasher.hpp"
|
||||
#include "src/hash/SHA512Hasher.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(SHA256HasherTest, KnownVectors) {
|
||||
SHA256Hasher hasher;
|
||||
EXPECT_EQ(hasher.hash(""),
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
EXPECT_EQ(hasher.hash(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
EXPECT_EQ(hasher.hash("password"),
|
||||
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8");
|
||||
EXPECT_EQ(hasher.hash("hello"),
|
||||
|
|
@ -68,8 +67,7 @@ TEST(SHA1HasherTest, StaticProperties) {
|
|||
|
||||
TEST(SHA512HasherTest, KnownVectors) {
|
||||
SHA512Hasher hasher;
|
||||
EXPECT_EQ(hasher.hash(""),
|
||||
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
|
||||
EXPECT_EQ(hasher.hash(""), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
|
||||
"47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
|
||||
EXPECT_EQ(hasher.hash("password"),
|
||||
"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9"
|
||||
|
|
|
|||
|
|
@ -17,18 +17,15 @@ Connects to:
|
|||
tests/data/small_wordlist.txt - fixture wordlist
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
#include "src/rules/RuleSet.hpp"
|
||||
#include <algorithm>
|
||||
#include <gtest/gtest.h>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
static std::vector<std::string> collect(std::generator<std::string> gen) {
|
||||
std::vector<std::string> out;
|
||||
for (auto&& s : gen) {
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
return out;
|
||||
return std::ranges::to<std::vector<std::string>>(std::move(gen));
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, CapitalizeFirst) {
|
||||
|
|
@ -103,10 +100,14 @@ TEST(RuleAttackTest, ChainRulesProducesMoreCandidates) {
|
|||
ASSERT_TRUE(with_chain.has_value());
|
||||
|
||||
std::size_t count_without = 0;
|
||||
while (without->next()) { ++count_without; }
|
||||
while (without->next()) {
|
||||
++count_without;
|
||||
}
|
||||
|
||||
std::size_t count_with = 0;
|
||||
while (with_chain->next()) { ++count_with; }
|
||||
while (with_chain->next()) {
|
||||
++count_with;
|
||||
}
|
||||
|
||||
EXPECT_GT(count_with, count_without);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -333,6 +333,7 @@ check_4_2_3() {
|
|||
fi
|
||||
|
||||
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
|
||||
|
||||
if [[ -z "$file_mode" ]]; then
|
||||
|
|
@ -369,6 +370,7 @@ check_4_2_4() {
|
|||
evidence="Found ${rule_count} logging rule(s) in rsyslog.conf"
|
||||
else
|
||||
local include_count
|
||||
# shellcheck disable=SC2016 # literal $IncludeConfig is regex syntax, not shell
|
||||
include_count=$(grep -cE '^\s*\$IncludeConfig|^\s*include\(' "$rsyslog_conf") || true
|
||||
|
||||
if [[ "$include_count" -gt 0 ]]; then
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ emit_json_report() {
|
|||
local status="${RESULT_STATUS[$id]}"
|
||||
local evidence="${RESULT_EVIDENCE[$id]:-}"
|
||||
local title="${CTRL_TITLE[$id]}"
|
||||
# shellcheck disable=SC2153 # CTRL_SECTION is a populated global associative array
|
||||
local ctrl_section="${CTRL_SECTION[$id]}"
|
||||
local level="${CTRL_LEVEL[$id]}"
|
||||
local scored="${CTRL_SCORED[$id]}"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ fail() { echo -e "${RED}[✖]${RESET} $1" >&2; exit "$EXIT_FAIL"; }
|
|||
|
||||
progress() {
|
||||
[[ "$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() {
|
||||
|
|
@ -120,6 +120,7 @@ service_is_active() {
|
|||
}
|
||||
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -138,11 +138,11 @@ test_baseline_missing_file() {
|
|||
CURRENT_TEST="test_baseline_missing_file"
|
||||
setup_test "${PROJECT_DIR}/testdata/fixtures"
|
||||
|
||||
local result
|
||||
result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) || true
|
||||
local result rc
|
||||
result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) && rc=0 || rc=$?
|
||||
|
||||
((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
|
||||
else
|
||||
((TEST_FAIL++)) || true
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ class IP_class {
|
|||
std::string dst;
|
||||
|
||||
public:
|
||||
std::string get_source();
|
||||
std::string get_dest();
|
||||
const std::string &get_source() const;
|
||||
const std::string &get_dest() const;
|
||||
// getters
|
||||
/*virtual std::string get_source() = 0;
|
||||
virtual std::string get_dest() = 0;*/
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ int main(int argc, char **argv) {
|
|||
/* get a filter, use vector for multiple */
|
||||
std::vector<filter> filters;
|
||||
if (parser.vm.contains("filter")) {
|
||||
auto &f = parser.vm["filter"].as<std::vector<std::string>>();
|
||||
for (auto &x : f) {
|
||||
const auto &f = parser.vm["filter"].as<std::vector<std::string>>();
|
||||
for (const auto &x : f) {
|
||||
filters.push_back(parse(x));
|
||||
filterString += x + " ";
|
||||
}
|
||||
|
|
@ -82,13 +82,10 @@ int main(int argc, char **argv) {
|
|||
/* our class for UI */
|
||||
View view;
|
||||
std::mutex render_mtx;
|
||||
std::mutex event_mtx;
|
||||
ftxui::Element current_render = isOffline
|
||||
? view.render(stats.get_snapshot(), interface, filterString, true, timer.load())
|
||||
: ftxui::text("Starting capture...");
|
||||
|
||||
std::mutex screen_mtx;
|
||||
|
||||
auto component = ftxui::Renderer([&] {
|
||||
std::lock_guard<std::mutex> lock(render_mtx);
|
||||
return current_render;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ void PcapCapture::stop() {
|
|||
/* print all available interfaces */
|
||||
void PcapCapture::print_interfaces() {
|
||||
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);
|
||||
if (dev->description != nullptr) {
|
||||
printf("(%s)\n", dev->description);
|
||||
|
|
@ -132,7 +132,7 @@ void PcapCapture::print_interfaces() {
|
|||
* @param packet Raw packet bytes
|
||||
*/
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@
|
|||
uint16_t IP_class::get_payload_len() const { return payload_len; }
|
||||
|
||||
TransportProtocol IP_class::get_protocol() const { return protocol; }
|
||||
std::string IP_class::get_source() { return src; }
|
||||
std::string IP_class::get_dest() { return dst; }
|
||||
const std::string &IP_class::get_source() const { return src; }
|
||||
const std::string &IP_class::get_dest() const { return dst; }
|
||||
|
||||
/*** Ipv4 ***/
|
||||
IPv4::IPv4(const u_char *data)
|
||||
: ip_hdr(reinterpret_cast<const ip *>(data)),
|
||||
ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
|
||||
: ip_hdr(reinterpret_cast<const ip *>(data)), ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
|
||||
std::array<char, INET_ADDRSTRLEN> src_buf{};
|
||||
inet_ntop(AF_INET, &ip_hdr->ip_src, src_buf.data(), sizeof(src_buf));
|
||||
src = src_buf.data();
|
||||
|
|
@ -84,8 +83,7 @@ uint16_t IPv4::get_dest_port() { return dest_port; }
|
|||
/*** Ipv6 ***/
|
||||
|
||||
IPv6::IPv6(const u_char *data)
|
||||
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)),
|
||||
ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
|
||||
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)), ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
|
||||
uint8_t hdr = ip_hdr->ip6_nxt;
|
||||
std::array<char, INET6_ADDRSTRLEN> src{};
|
||||
inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src));
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ ApplicationProtocol Packet::get_application_protocol() {
|
|||
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) {
|
||||
return ApplicationProtocol::HTTPS;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#include "ftxui/dom/table.hpp"
|
||||
#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.
|
||||
*
|
||||
|
|
@ -96,7 +96,7 @@ void Stats::update_transport_stats() {
|
|||
snapshot.transport_rows.push_back({"Proto", "Packets", "Bytes", "%"});
|
||||
|
||||
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) {
|
||||
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::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.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"});
|
||||
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;
|
||||
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) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
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.push_back({"Source", "Destination", "bytes received", "%"});
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ namespace po = boost::program_options;
|
|||
int main(int argc, char* argv[]) {
|
||||
|
||||
po::options_description desc("Allowed options");
|
||||
desc.add_options()
|
||||
("help,h", "produce help message")
|
||||
("dname,i", po::value<std::string>()->default_value("127.0.0.1"), "set domain name or IP address")
|
||||
("ports,p", po::value<std::string>()->default_value("1-1024"), "set a port range from 1 to n")
|
||||
("threads,t", po::value<int>()->default_value(100), "max concurrent threads")
|
||||
("expiry_time,e", po::value<uint8_t>()->default_value(2)->value_name("sec"), "timeout in seconds")
|
||||
("verbose,v", "verbose output");
|
||||
desc.add_options()("help,h", "produce help message")(
|
||||
"dname,i", po::value<std::string>()->default_value("127.0.0.1"),
|
||||
"set domain name or IP address")("ports,p",
|
||||
po::value<std::string>()->default_value("1-1024"),
|
||||
"set a port range from 1 to n")(
|
||||
"threads,t", po::value<int>()->default_value(100), "max concurrent threads")(
|
||||
"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::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,10 @@
|
|||
#include "PortScanner.hpp"
|
||||
|
||||
const std::unordered_map<uint16_t, std::string> PortScanner::basicPorts{
|
||||
{21, "FTP"},
|
||||
{22, "SSH"},
|
||||
{23, "TelNet"},
|
||||
{25, "SMTP"},
|
||||
{53, "DNS"},
|
||||
{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"},
|
||||
{21, "FTP"}, {22, "SSH"}, {23, "TelNet"}, {25, "SMTP"}, {53, "DNS"},
|
||||
{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"},
|
||||
};
|
||||
|
||||
|
|
@ -46,19 +30,18 @@ void PortScanner::parse_port(std::string& port) {
|
|||
++it;
|
||||
}
|
||||
|
||||
int start = std::stoi(s);
|
||||
int end = std::stoi(e);
|
||||
//check a valid bounds
|
||||
if (start == 0 || end > MAX_PORT || start > end) {
|
||||
int start_port = std::stoi(s);
|
||||
int end_port = std::stoi(e);
|
||||
if (start_port == 0 || end_port > MAX_PORT || start_port > end_port) {
|
||||
startPort = 1;
|
||||
endPort = MAX_PORT;
|
||||
}
|
||||
else {
|
||||
startPort = static_cast<uint16_t>(start);
|
||||
endPort = static_cast<uint16_t>(end);
|
||||
} else {
|
||||
startPort = static_cast<uint16_t>(start_port);
|
||||
endPort = static_cast<uint16_t>(end_port);
|
||||
}
|
||||
}
|
||||
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,
|
||||
std::uint8_t expiry_time) {
|
||||
this->domainName = std::move(domainName);
|
||||
this->MAX_THREADS = max_threads;
|
||||
this->expiry_time = expiry_time;
|
||||
|
|
@ -68,7 +51,6 @@ PortScanner::PortScanner(std::string& domainName, std::string& port, int max_thr
|
|||
endpoint = *result.begin();
|
||||
|
||||
setup_queue();
|
||||
|
||||
}
|
||||
|
||||
void PortScanner::setup_queue() {
|
||||
|
|
@ -78,18 +60,15 @@ void PortScanner::setup_queue() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void PortScanner::set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time) {
|
||||
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);
|
||||
|
||||
|
||||
auto result = resolver.resolve(this->domainName, "");
|
||||
endpoint = *result.begin();
|
||||
|
||||
|
||||
}
|
||||
|
||||
void PortScanner::set_max_port(std::uint16_t port) {
|
||||
|
|
@ -101,7 +80,6 @@ void PortScanner::set_max_threads(int value) {
|
|||
|
||||
void PortScanner::set_ip_address(std::string ip) {
|
||||
domainName = std::move(ip);
|
||||
|
||||
}
|
||||
|
||||
void PortScanner::set_expiry_time(std::uint8_t value) {
|
||||
|
|
@ -111,10 +89,7 @@ void PortScanner::set_expiry_time(std::uint8_t value) {
|
|||
void PortScanner::start() {
|
||||
setup_queue();
|
||||
for (int i = 0; i < MAX_THREADS; i++) {
|
||||
boost::asio::post(strand, [this]() {
|
||||
scan();
|
||||
});
|
||||
|
||||
boost::asio::post(strand, [this]() { scan(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +103,8 @@ void PortScanner::run() {
|
|||
}
|
||||
|
||||
void PortScanner::scan() {
|
||||
if (q.empty() || cnt >= MAX_THREADS) return;
|
||||
if (q.empty() || cnt >= MAX_THREADS)
|
||||
return;
|
||||
|
||||
uint16_t port = q.front();
|
||||
q.pop();
|
||||
|
|
@ -138,11 +114,12 @@ void PortScanner::scan() {
|
|||
auto timer = std::make_shared<boost::asio::steady_timer>(io);
|
||||
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->async_wait(boost::asio::bind_executor(strand, [this, complete, socket, port](boost::system::error_code ec) {
|
||||
timer->async_wait(boost::asio::bind_executor(
|
||||
strand, [this, complete, socket, port](boost::system::error_code ec) {
|
||||
if (!ec && !*complete) {
|
||||
*complete = true;
|
||||
socket->close();
|
||||
|
|
@ -151,11 +128,13 @@ void PortScanner::scan() {
|
|||
--cnt;
|
||||
scan();
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
socket->async_connect(endpoint, boost::asio::bind_executor(strand, [this,socket, timer, port, complete](boost::system::error_code ec) {
|
||||
if (*complete) return;
|
||||
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();
|
||||
|
||||
|
|
@ -169,25 +148,27 @@ void PortScanner::scan() {
|
|||
if (!ec) {
|
||||
auto buf = std::make_shared<std::array<char, 128>>();
|
||||
|
||||
socket->async_read_some(boost::asio::buffer(*buf),boost::asio::bind_executor(strand,
|
||||
[this, port, buf, banner, service](boost::system::error_code ec, std::size_t n) {
|
||||
socket->async_read_some(
|
||||
boost::asio::buffer(*buf),
|
||||
boost::asio::bind_executor(
|
||||
strand, [this, port, buf, banner, service](
|
||||
boost::system::error_code ec, std::size_t n) {
|
||||
if (!ec && n > 0) {
|
||||
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,
|
||||
service.c_str(), banner->c_str());
|
||||
++open_ports;
|
||||
--cnt;
|
||||
scan();
|
||||
}));
|
||||
|
||||
}
|
||||
else{
|
||||
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET, service.c_str(), banner->c_str());
|
||||
} else {
|
||||
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET,
|
||||
service.c_str(), banner->c_str());
|
||||
++closed_ports;
|
||||
--cnt;
|
||||
scan();
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#ifndef PORTSCANNER_HPP
|
||||
#define PORTSCANNER_HPP
|
||||
#include <boost/asio.hpp>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <boost/asio.hpp>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#define RED "\033[31m"
|
||||
#define GREEN "\033[32m"
|
||||
|
|
@ -38,14 +38,17 @@ private:
|
|||
void scan();
|
||||
void setup_queue();
|
||||
void parse_port(std::string& port);
|
||||
public:
|
||||
PortScanner(std::string& ip_address, std::string& port, int max_threads, std::uint8_t expiry_time);
|
||||
PortScanner(){}
|
||||
~PortScanner() {
|
||||
|
||||
public:
|
||||
PortScanner(std::string& ip_address, std::string& port, int max_threads,
|
||||
std::uint8_t expiry_time);
|
||||
PortScanner() {
|
||||
}
|
||||
~PortScanner() {
|
||||
}
|
||||
|
||||
void set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time);
|
||||
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);
|
||||
|
|
@ -55,4 +58,4 @@ public:
|
|||
void run();
|
||||
};
|
||||
|
||||
#endif //PORTSCANNER_HPP
|
||||
#endif // PORTSCANNER_HPP
|
||||
|
|
|
|||
|
|
@ -61,12 +61,7 @@ pub struct AnalysisContext {
|
|||
}
|
||||
|
||||
impl AnalysisContext {
|
||||
pub fn new(
|
||||
source: BinarySource,
|
||||
sha256: String,
|
||||
file_name: String,
|
||||
file_size: u64,
|
||||
) -> Self {
|
||||
pub fn new(source: BinarySource, sha256: String, file_name: String, file_size: u64) -> Self {
|
||||
Self {
|
||||
source,
|
||||
sha256,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ pub enum EngineError {
|
|||
UnsupportedArchitecture { arch: String },
|
||||
|
||||
#[error("pass '{pass}' missing dependency: {dependency}")]
|
||||
MissingDependency {
|
||||
pass: String,
|
||||
dependency: String,
|
||||
},
|
||||
MissingDependency { pass: String, dependency: String },
|
||||
|
||||
#[error("pass '{pass}' failed")]
|
||||
PassFailed {
|
||||
|
|
|
|||
|
|
@ -25,31 +25,23 @@
|
|||
// types.rs - Architecture, BinaryFormat, Endianness,
|
||||
// SectionPermissions
|
||||
|
||||
use goblin::elf::Elf;
|
||||
use goblin::elf::dynamic::DT_BIND_NOW;
|
||||
use goblin::elf::header::{
|
||||
EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN,
|
||||
ET_EXEC, ET_REL,
|
||||
EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN, ET_EXEC, ET_REL,
|
||||
};
|
||||
use goblin::elf::program_header::{
|
||||
PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME,
|
||||
PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD, PT_NOTE,
|
||||
PT_NULL, PT_PHDR,
|
||||
};
|
||||
use goblin::elf::section_header::{
|
||||
SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS,
|
||||
SHT_SYMTAB,
|
||||
PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME, PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD,
|
||||
PT_NOTE, PT_NULL, PT_PHDR,
|
||||
};
|
||||
use goblin::elf::section_header::{SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS, SHT_SYMTAB};
|
||||
use goblin::elf::sym::STT_FUNC;
|
||||
use goblin::elf::Elf;
|
||||
|
||||
use super::{
|
||||
detect_common_anomalies, compute_section_hash, ElfInfo,
|
||||
FormatResult, SectionInfo, SegmentInfo,
|
||||
ElfInfo, FormatResult, SectionInfo, SegmentInfo, compute_section_hash, detect_common_anomalies,
|
||||
};
|
||||
use crate::error::EngineError;
|
||||
use crate::types::{
|
||||
Architecture, BinaryFormat, Endianness, SectionPermissions,
|
||||
};
|
||||
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
|
||||
|
||||
const EI_OSABI: usize = 7;
|
||||
const ELFOSABI_NONE: u8 = 0;
|
||||
|
|
@ -65,12 +57,8 @@ const ELFOSABI_STANDALONE: u8 = 255;
|
|||
const DT_FLAGS: u64 = 30;
|
||||
const DF_BIND_NOW: u64 = 0x8;
|
||||
|
||||
pub fn parse_elf(
|
||||
elf: &Elf,
|
||||
data: &[u8],
|
||||
) -> Result<FormatResult, EngineError> {
|
||||
let architecture =
|
||||
map_architecture(elf.header.e_machine);
|
||||
pub fn parse_elf(elf: &Elf, data: &[u8]) -> Result<FormatResult, EngineError> {
|
||||
let architecture = map_architecture(elf.header.e_machine);
|
||||
let bits = if elf.is_64 { 64 } else { 32 };
|
||||
let endianness = if elf.little_endian {
|
||||
Endianness::Little
|
||||
|
|
@ -85,32 +73,20 @@ pub fn parse_elf(
|
|||
.any(|sh| sh.sh_type == SHT_SYMTAB);
|
||||
let is_stripped = !has_symtab;
|
||||
|
||||
let has_interp = elf
|
||||
.program_headers
|
||||
.iter()
|
||||
.any(|ph| ph.p_type == PT_INTERP);
|
||||
let is_pie =
|
||||
elf.header.e_type == ET_DYN && has_interp;
|
||||
let has_interp = elf.program_headers.iter().any(|ph| ph.p_type == PT_INTERP);
|
||||
let is_pie = elf.header.e_type == ET_DYN && has_interp;
|
||||
|
||||
let has_debug_info =
|
||||
elf.section_headers.iter().any(|sh| {
|
||||
let has_debug_info = elf.section_headers.iter().any(|sh| {
|
||||
elf.shdr_strtab
|
||||
.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 segments = build_segments(elf);
|
||||
let anomalies = detect_common_anomalies(
|
||||
§ions,
|
||||
entry_point,
|
||||
is_stripped,
|
||||
);
|
||||
let anomalies = detect_common_anomalies(§ions, entry_point, is_stripped);
|
||||
let elf_info = build_elf_info(elf);
|
||||
let function_hints =
|
||||
collect_function_hints(elf, entry_point);
|
||||
let function_hints = collect_function_hints(elf, entry_point);
|
||||
|
||||
Ok(FormatResult {
|
||||
format: BinaryFormat::Elf,
|
||||
|
|
@ -137,11 +113,7 @@ fn map_architecture(machine: u16) -> Architecture {
|
|||
EM_X86_64 => Architecture::X86_64,
|
||||
EM_ARM => Architecture::Arm,
|
||||
EM_AARCH64 => Architecture::Aarch64,
|
||||
other => {
|
||||
Architecture::Other(format!(
|
||||
"elf-machine-{other}"
|
||||
))
|
||||
}
|
||||
other => Architecture::Other(format!("elf-machine-{other}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,10 +160,7 @@ fn segment_type_name(p_type: u32) -> Option<String> {
|
|||
)
|
||||
}
|
||||
|
||||
fn build_sections(
|
||||
elf: &Elf,
|
||||
data: &[u8],
|
||||
) -> Vec<SectionInfo> {
|
||||
fn build_sections(elf: &Elf, data: &[u8]) -> Vec<SectionInfo> {
|
||||
elf.section_headers
|
||||
.iter()
|
||||
.skip(1)
|
||||
|
|
@ -203,32 +172,16 @@ fn build_sections(
|
|||
.to_string();
|
||||
|
||||
let is_nobits = shdr.sh_type == SHT_NOBITS;
|
||||
let raw_offset = if is_nobits {
|
||||
0
|
||||
} else {
|
||||
shdr.sh_offset
|
||||
};
|
||||
let raw_size = if is_nobits {
|
||||
0
|
||||
} else {
|
||||
shdr.sh_size
|
||||
};
|
||||
let raw_offset = if is_nobits { 0 } else { shdr.sh_offset };
|
||||
let raw_size = if is_nobits { 0 } else { shdr.sh_size };
|
||||
|
||||
let permissions = SectionPermissions {
|
||||
read: (shdr.sh_flags
|
||||
& u64::from(SHF_ALLOC))
|
||||
!= 0,
|
||||
write: (shdr.sh_flags
|
||||
& u64::from(SHF_WRITE))
|
||||
!= 0,
|
||||
execute: (shdr.sh_flags
|
||||
& u64::from(SHF_EXECINSTR))
|
||||
!= 0,
|
||||
read: (shdr.sh_flags & u64::from(SHF_ALLOC)) != 0,
|
||||
write: (shdr.sh_flags & u64::from(SHF_WRITE)) != 0,
|
||||
execute: (shdr.sh_flags & u64::from(SHF_EXECINSTR)) != 0,
|
||||
};
|
||||
|
||||
let sha256 = compute_section_hash(
|
||||
data, raw_offset, raw_size,
|
||||
);
|
||||
let sha256 = compute_section_hash(data, raw_offset, raw_size);
|
||||
|
||||
SectionInfo {
|
||||
name,
|
||||
|
|
@ -267,11 +220,9 @@ fn build_segments(elf: &Elf) -> Vec<SegmentInfo> {
|
|||
}
|
||||
|
||||
fn build_elf_info(elf: &Elf) -> ElfInfo {
|
||||
let os_abi =
|
||||
os_abi_name(elf.header.e_ident[EI_OSABI]);
|
||||
let os_abi = os_abi_name(elf.header.e_ident[EI_OSABI]);
|
||||
let elf_type = elf_type_name(elf.header.e_type);
|
||||
let interpreter =
|
||||
elf.interpreter.map(|s| s.to_string());
|
||||
let interpreter = elf.interpreter.map(|s| s.to_string());
|
||||
|
||||
let gnu_relro = elf
|
||||
.program_headers
|
||||
|
|
@ -287,23 +238,17 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
|
|||
let mut bind_now = false;
|
||||
if let Some(dynamic) = &elf.dynamic {
|
||||
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 {
|
||||
bind_now = true;
|
||||
}
|
||||
if tag == DT_FLAGS
|
||||
&& (dyn_entry.d_val & DF_BIND_NOW) != 0
|
||||
{
|
||||
if tag == DT_FLAGS && (dyn_entry.d_val & DF_BIND_NOW) != 0 {
|
||||
bind_now = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let needed_libraries = elf
|
||||
.libraries
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let needed_libraries = elf.libraries.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
ElfInfo {
|
||||
os_abi,
|
||||
|
|
@ -316,19 +261,12 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
|
|||
}
|
||||
}
|
||||
|
||||
fn collect_function_hints(
|
||||
elf: &Elf,
|
||||
entry_point: u64,
|
||||
) -> Vec<u64> {
|
||||
fn collect_function_hints(elf: &Elf, entry_point: u64) -> Vec<u64> {
|
||||
let mut hints: Vec<u64> = elf
|
||||
.syms
|
||||
.iter()
|
||||
.chain(elf.dynsyms.iter())
|
||||
.filter(|sym| {
|
||||
sym.st_type() == STT_FUNC
|
||||
&& sym.st_value != 0
|
||||
&& sym.st_value != entry_point
|
||||
})
|
||||
.filter(|sym| sym.st_type() == STT_FUNC && sym.st_value != 0 && sym.st_value != entry_point)
|
||||
.map(|sym| sym.st_value)
|
||||
.collect();
|
||||
hints.sort_unstable();
|
||||
|
|
|
|||
|
|
@ -26,21 +26,16 @@
|
|||
// types.rs - Architecture, BinaryFormat, Endianness,
|
||||
// SectionPermissions
|
||||
|
||||
use goblin::mach::cputype::{
|
||||
CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86,
|
||||
CPU_TYPE_X86_64,
|
||||
};
|
||||
use goblin::mach::cputype::{CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86, CPU_TYPE_X86_64};
|
||||
use goblin::mach::load_command::CommandVariant;
|
||||
use goblin::mach::{Mach, MachO};
|
||||
|
||||
use super::{
|
||||
compute_section_hash, detect_common_anomalies,
|
||||
FormatResult, MachOInfo, SectionInfo, SegmentInfo,
|
||||
FormatResult, MachOInfo, SectionInfo, SegmentInfo, compute_section_hash,
|
||||
detect_common_anomalies,
|
||||
};
|
||||
use crate::error::EngineError;
|
||||
use crate::types::{
|
||||
Architecture, BinaryFormat, Endianness, SectionPermissions,
|
||||
};
|
||||
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
|
||||
|
||||
const MH_OBJECT: u32 = 1;
|
||||
const MH_EXECUTE: u32 = 2;
|
||||
|
|
@ -53,34 +48,23 @@ const VM_PROT_READ: u32 = 0x01;
|
|||
const VM_PROT_WRITE: u32 = 0x02;
|
||||
const VM_PROT_EXECUTE: u32 = 0x04;
|
||||
|
||||
pub fn parse_macho(
|
||||
mach: &Mach,
|
||||
data: &[u8],
|
||||
) -> Result<FormatResult, EngineError> {
|
||||
pub fn parse_macho(mach: &Mach, data: &[u8]) -> Result<FormatResult, EngineError> {
|
||||
match mach {
|
||||
Mach::Binary(macho) => {
|
||||
parse_single_macho(macho, data, false)
|
||||
}
|
||||
Mach::Binary(macho) => parse_single_macho(macho, data, false),
|
||||
Mach::Fat(fat) => {
|
||||
for arch in fat.iter_arches() {
|
||||
let arch = arch.map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
let arch = arch.map_err(|e| EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let offset = arch.offset as usize;
|
||||
let size = arch.size as usize;
|
||||
let end = offset.saturating_add(size);
|
||||
if end <= data.len() {
|
||||
let macho = MachO::parse(data, offset)
|
||||
.map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
let macho =
|
||||
MachO::parse(data, offset).map_err(|e| EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
return parse_single_macho(
|
||||
&macho, data, true,
|
||||
);
|
||||
return parse_single_macho(&macho, data, true);
|
||||
}
|
||||
}
|
||||
Err(EngineError::InvalidBinary {
|
||||
|
|
@ -97,8 +81,7 @@ fn parse_single_macho(
|
|||
data: &[u8],
|
||||
is_universal: bool,
|
||||
) -> Result<FormatResult, EngineError> {
|
||||
let architecture =
|
||||
map_architecture(macho.header.cputype);
|
||||
let architecture = map_architecture(macho.header.cputype);
|
||||
let bits = if macho.is_64 { 64 } else { 32 };
|
||||
let endianness = if macho.little_endian {
|
||||
Endianness::Little
|
||||
|
|
@ -107,25 +90,20 @@ fn parse_single_macho(
|
|||
};
|
||||
let entry_point = macho.entry;
|
||||
|
||||
let symbols: Vec<_> =
|
||||
macho.symbols().flatten().collect();
|
||||
let symbols: Vec<_> = macho.symbols().flatten().collect();
|
||||
let is_stripped = symbols.is_empty();
|
||||
|
||||
let has_debug_info = macho.segments.iter().any(|seg| {
|
||||
seg.name().is_ok_and(|n| n == "__DWARF")
|
||||
});
|
||||
let has_debug_info = macho
|
||||
.segments
|
||||
.iter()
|
||||
.any(|seg| seg.name().is_ok_and(|n| n == "__DWARF"));
|
||||
|
||||
let is_pie = macho.header.flags & 0x0020_0000 != 0;
|
||||
|
||||
let sections = build_sections(macho, data);
|
||||
let segments = build_segments(macho);
|
||||
let anomalies = detect_common_anomalies(
|
||||
§ions,
|
||||
entry_point,
|
||||
is_stripped,
|
||||
);
|
||||
let macho_info =
|
||||
build_macho_info(macho, is_universal);
|
||||
let anomalies = detect_common_anomalies(§ions, entry_point, is_stripped);
|
||||
let macho_info = build_macho_info(macho, is_universal);
|
||||
|
||||
let function_hints: Vec<u64> = macho
|
||||
.symbols()
|
||||
|
|
@ -164,11 +142,7 @@ fn map_architecture(cputype: u32) -> Architecture {
|
|||
CPU_TYPE_X86_64 => Architecture::X86_64,
|
||||
CPU_TYPE_ARM => Architecture::Arm,
|
||||
CPU_TYPE_ARM64 => Architecture::Aarch64,
|
||||
other => {
|
||||
Architecture::Other(format!(
|
||||
"mach-cpu-{other:#x}"
|
||||
))
|
||||
}
|
||||
other => Architecture::Other(format!("mach-cpu-{other:#x}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,14 +158,10 @@ fn file_type_name(filetype: u32) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn cpu_subtype_name(
|
||||
cputype: u32,
|
||||
cpusubtype: u32,
|
||||
) -> String {
|
||||
fn cpu_subtype_name(cputype: u32, cpusubtype: u32) -> String {
|
||||
let subtype = cpusubtype & 0x00FF_FFFF;
|
||||
match cputype {
|
||||
CPU_TYPE_X86 | CPU_TYPE_X86_64 => {
|
||||
match subtype {
|
||||
CPU_TYPE_X86 | CPU_TYPE_X86_64 => match subtype {
|
||||
3 => "ALL".into(),
|
||||
4 => "486".into(),
|
||||
8 => "PENTIUM_3".into(),
|
||||
|
|
@ -200,8 +170,7 @@ fn cpu_subtype_name(
|
|||
11 => "ITANIUM".into(),
|
||||
12 => "XEON".into(),
|
||||
_ => format!("{subtype}"),
|
||||
}
|
||||
}
|
||||
},
|
||||
CPU_TYPE_ARM => match subtype {
|
||||
6 => "v6".into(),
|
||||
9 => "v7".into(),
|
||||
|
|
@ -220,34 +189,23 @@ fn cpu_subtype_name(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_sections(
|
||||
macho: &MachO,
|
||||
data: &[u8],
|
||||
) -> Vec<SectionInfo> {
|
||||
fn build_sections(macho: &MachO, data: &[u8]) -> Vec<SectionInfo> {
|
||||
let mut sections = Vec::new();
|
||||
for segment in macho.segments.iter() {
|
||||
let initprot = segment.initprot;
|
||||
let seg_permissions = SectionPermissions {
|
||||
read: (initprot & VM_PROT_READ) != 0,
|
||||
write: (initprot & VM_PROT_WRITE) != 0,
|
||||
execute: (initprot & VM_PROT_EXECUTE)
|
||||
!= 0,
|
||||
execute: (initprot & VM_PROT_EXECUTE) != 0,
|
||||
};
|
||||
for section_result in segment.into_iter() {
|
||||
let Ok((section, _section_data)) =
|
||||
section_result
|
||||
else {
|
||||
let Ok((section, _section_data)) = section_result else {
|
||||
continue;
|
||||
};
|
||||
let name = section
|
||||
.name()
|
||||
.unwrap_or("???")
|
||||
.to_string();
|
||||
let name = section.name().unwrap_or("???").to_string();
|
||||
let raw_offset = section.offset as u64;
|
||||
let raw_size = section.size;
|
||||
let sha256 = compute_section_hash(
|
||||
data, raw_offset, raw_size,
|
||||
);
|
||||
let sha256 = compute_section_hash(data, raw_offset, raw_size);
|
||||
|
||||
sections.push(SectionInfo {
|
||||
name,
|
||||
|
|
@ -268,16 +226,12 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
|
|||
.segments
|
||||
.iter()
|
||||
.map(|seg| {
|
||||
let name = seg
|
||||
.name()
|
||||
.ok()
|
||||
.map(|n| n.to_string());
|
||||
let name = seg.name().ok().map(|n| n.to_string());
|
||||
let initprot = seg.initprot;
|
||||
let permissions = SectionPermissions {
|
||||
read: (initprot & VM_PROT_READ) != 0,
|
||||
write: (initprot & VM_PROT_WRITE) != 0,
|
||||
execute: (initprot & VM_PROT_EXECUTE)
|
||||
!= 0,
|
||||
execute: (initprot & VM_PROT_EXECUTE) != 0,
|
||||
};
|
||||
|
||||
SegmentInfo {
|
||||
|
|
@ -292,16 +246,9 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn build_macho_info(
|
||||
macho: &MachO,
|
||||
is_universal: bool,
|
||||
) -> MachOInfo {
|
||||
let file_type =
|
||||
file_type_name(macho.header.filetype);
|
||||
let cpu_subtype = cpu_subtype_name(
|
||||
macho.header.cputype,
|
||||
macho.header.cpusubtype,
|
||||
);
|
||||
fn build_macho_info(macho: &MachO, is_universal: bool) -> 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_function_starts = false;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ use serde::{Deserialize, Serialize};
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::error::EngineError;
|
||||
use crate::types::{
|
||||
Architecture, BinaryFormat, Endianness, SectionPermissions,
|
||||
};
|
||||
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
|
||||
|
||||
pub const SUSPICIOUS_SECTION_NAMES: &[(&str, &str)] = &[
|
||||
("UPX0", "UPX packer"),
|
||||
|
|
@ -192,37 +190,22 @@ pub struct MachOInfo {
|
|||
pub has_function_starts: bool,
|
||||
}
|
||||
|
||||
pub fn parse_format(
|
||||
data: &[u8],
|
||||
) -> Result<FormatResult, EngineError> {
|
||||
let object =
|
||||
goblin::Object::parse(data).map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
pub fn parse_format(data: &[u8]) -> Result<FormatResult, EngineError> {
|
||||
let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match &object {
|
||||
goblin::Object::Elf(elf_obj) => {
|
||||
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::Elf(elf_obj) => 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),
|
||||
_ => Err(EngineError::UnsupportedFormat {
|
||||
format: "unknown".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_section_hash(
|
||||
data: &[u8],
|
||||
offset: u64,
|
||||
size: u64,
|
||||
) -> String {
|
||||
fn compute_section_hash(data: &[u8], offset: u64, size: u64) -> String {
|
||||
if size == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
|
@ -251,53 +234,32 @@ fn detect_common_anomalies(
|
|||
) -> Vec<FormatAnomaly> {
|
||||
let mut anomalies = Vec::new();
|
||||
|
||||
let text_section =
|
||||
sections.iter().find(|s| s.name == ".text");
|
||||
let text_section = sections.iter().find(|s| s.name == ".text");
|
||||
if let Some(text) = text_section {
|
||||
let text_end =
|
||||
text.virtual_address + text.virtual_size;
|
||||
if entry_point != 0
|
||||
&& (entry_point < text.virtual_address
|
||||
|| entry_point >= text_end)
|
||||
{
|
||||
anomalies.push(
|
||||
FormatAnomaly::EntryPointOutsideText {
|
||||
let text_end = text.virtual_address + text.virtual_size;
|
||||
if entry_point != 0 && (entry_point < text.virtual_address || entry_point >= text_end) {
|
||||
anomalies.push(FormatAnomaly::EntryPointOutsideText {
|
||||
ep: entry_point,
|
||||
text_range: (
|
||||
text.virtual_address,
|
||||
text_end,
|
||||
),
|
||||
},
|
||||
);
|
||||
text_range: (text.virtual_address, text_end),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last) = sections.last() {
|
||||
let last_end =
|
||||
last.virtual_address + last.virtual_size;
|
||||
if entry_point >= last.virtual_address
|
||||
&& entry_point < last_end
|
||||
{
|
||||
anomalies.push(
|
||||
FormatAnomaly::EntryPointInLastSection {
|
||||
let last_end = last.virtual_address + last.virtual_size;
|
||||
if entry_point >= last.virtual_address && entry_point < last_end {
|
||||
anomalies.push(FormatAnomaly::EntryPointInLastSection {
|
||||
ep: entry_point,
|
||||
section: last.name.clone(),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let ep_in_any = sections.iter().any(|s| {
|
||||
entry_point >= s.virtual_address
|
||||
&& entry_point
|
||||
< s.virtual_address + s.virtual_size
|
||||
entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
|
||||
});
|
||||
if !ep_in_any && entry_point != 0 {
|
||||
anomalies.push(
|
||||
FormatAnomaly::EntryPointOutsideSections {
|
||||
ep: entry_point,
|
||||
},
|
||||
);
|
||||
anomalies.push(FormatAnomaly::EntryPointOutsideSections { ep: entry_point });
|
||||
}
|
||||
|
||||
for (idx, section) in sections.iter().enumerate() {
|
||||
|
|
@ -308,47 +270,31 @@ fn detect_common_anomalies(
|
|||
}
|
||||
|
||||
if section.name.is_empty() {
|
||||
anomalies.push(
|
||||
FormatAnomaly::EmptySectionName {
|
||||
index: idx,
|
||||
},
|
||||
);
|
||||
anomalies.push(FormatAnomaly::EmptySectionName { index: idx });
|
||||
}
|
||||
|
||||
if let Some(reason) =
|
||||
check_suspicious_name(§ion.name)
|
||||
{
|
||||
anomalies.push(
|
||||
FormatAnomaly::SuspiciousSectionName {
|
||||
if let Some(reason) = check_suspicious_name(§ion.name) {
|
||||
anomalies.push(FormatAnomaly::SuspiciousSectionName {
|
||||
name: section.name.clone(),
|
||||
reason,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if section.permissions.execute
|
||||
&& section.virtual_size == 0
|
||||
{
|
||||
anomalies.push(
|
||||
FormatAnomaly::ZeroSizeCodeSection {
|
||||
if section.permissions.execute && section.virtual_size == 0 {
|
||||
anomalies.push(FormatAnomaly::ZeroSizeCodeSection {
|
||||
name: section.name.clone(),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if section.raw_size > 0 {
|
||||
let ratio = section.virtual_size as f64
|
||||
/ section.raw_size as f64;
|
||||
let ratio = section.virtual_size as f64 / section.raw_size as f64;
|
||||
if ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
|
||||
anomalies.push(
|
||||
FormatAnomaly::VirtualRawSizeMismatch {
|
||||
anomalies.push(FormatAnomaly::VirtualRawSizeMismatch {
|
||||
name: section.name.clone(),
|
||||
virtual_size: section
|
||||
.virtual_size,
|
||||
virtual_size: section.virtual_size,
|
||||
raw_size: section.raw_size,
|
||||
ratio,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,14 +28,11 @@
|
|||
use goblin::pe::PE;
|
||||
|
||||
use super::{
|
||||
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo, SectionInfo, SegmentInfo,
|
||||
compute_section_hash, detect_common_anomalies,
|
||||
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo,
|
||||
SectionInfo, SegmentInfo,
|
||||
};
|
||||
use crate::error::EngineError;
|
||||
use crate::types::{
|
||||
Architecture, BinaryFormat, Endianness, SectionPermissions,
|
||||
};
|
||||
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
|
||||
|
||||
const COFF_MACHINE_I386: u16 = 0x14c;
|
||||
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_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040;
|
||||
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 =
|
||||
0x0080;
|
||||
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = 0x0080;
|
||||
const IMAGE_DLLCHARACTERISTICS_NX_COMPAT: u16 = 0x0100;
|
||||
const IMAGE_DLLCHARACTERISTICS_NO_SEH: u16 = 0x0400;
|
||||
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";
|
||||
|
||||
pub fn parse_pe(
|
||||
pe: &PE,
|
||||
data: &[u8],
|
||||
) -> Result<FormatResult, EngineError> {
|
||||
let architecture = map_architecture(
|
||||
pe.header.coff_header.machine,
|
||||
);
|
||||
pub fn parse_pe(pe: &PE, data: &[u8]) -> Result<FormatResult, EngineError> {
|
||||
let architecture = map_architecture(pe.header.coff_header.machine);
|
||||
let bits = if pe.is_64 { 64 } else { 32 };
|
||||
let endianness = Endianness::Little;
|
||||
let entry_point = pe.entry as u64;
|
||||
|
||||
let is_stripped = pe.debug_data.is_none();
|
||||
let optional = pe.header.optional_header.as_ref();
|
||||
let dll_chars = optional.map_or(0, |oh| {
|
||||
oh.windows_fields.dll_characteristics
|
||||
});
|
||||
let is_pie = (dll_chars
|
||||
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
|
||||
!= 0;
|
||||
let dll_chars = optional.map_or(0, |oh| oh.windows_fields.dll_characteristics);
|
||||
let is_pie = (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0;
|
||||
let has_debug_info = pe.debug_data.is_some();
|
||||
|
||||
let sections = build_sections(pe, data);
|
||||
let segments = build_segments(pe);
|
||||
|
||||
let timestamp =
|
||||
pe.header.coff_header.time_date_stamp;
|
||||
let image_base = optional
|
||||
.map_or(0, |oh| oh.windows_fields.image_base);
|
||||
let subsystem_raw = optional
|
||||
.map_or(0, |oh| oh.windows_fields.subsystem);
|
||||
let timestamp = pe.header.coff_header.time_date_stamp;
|
||||
let image_base = optional.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(
|
||||
|| "0.0".into(),
|
||||
|oh| {
|
||||
format!(
|
||||
"{}.{}",
|
||||
oh.standard_fields.major_linker_version,
|
||||
oh.standard_fields.minor_linker_version,
|
||||
oh.standard_fields.major_linker_version, oh.standard_fields.minor_linker_version,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
|
@ -117,47 +100,29 @@ pub fn parse_pe(
|
|||
let max_section_end = pe
|
||||
.sections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.pointer_to_raw_data as u64
|
||||
+ s.size_of_raw_data as u64
|
||||
})
|
||||
.map(|s| s.pointer_to_raw_data as u64 + s.size_of_raw_data as u64)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let file_size = data.len() as u64;
|
||||
let has_overlay =
|
||||
max_section_end > 0 && max_section_end < file_size;
|
||||
let has_overlay = max_section_end > 0 && max_section_end < file_size;
|
||||
let overlay_size = if has_overlay {
|
||||
file_size - max_section_end
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let pe_offset =
|
||||
pe.header.dos_header.pe_pointer as usize;
|
||||
let rich_header_present = detect_rich_header(
|
||||
data,
|
||||
pe_offset,
|
||||
);
|
||||
let pe_offset = pe.header.dos_header.pe_pointer as usize;
|
||||
let rich_header_present = detect_rich_header(data, pe_offset);
|
||||
|
||||
let pe_info = PeInfo {
|
||||
image_base,
|
||||
subsystem: subsystem_name(subsystem_raw),
|
||||
dll_characteristics: PeDllCharacteristics {
|
||||
aslr: (dll_chars
|
||||
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
|
||||
!= 0,
|
||||
dep: (dll_chars
|
||||
& IMAGE_DLLCHARACTERISTICS_NX_COMPAT)
|
||||
!= 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,
|
||||
aslr: (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0,
|
||||
dep: (dll_chars & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) != 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,
|
||||
linker_version,
|
||||
|
|
@ -167,11 +132,7 @@ pub fn parse_pe(
|
|||
rich_header_present,
|
||||
};
|
||||
|
||||
let mut anomalies = detect_common_anomalies(
|
||||
§ions,
|
||||
entry_point,
|
||||
is_stripped,
|
||||
);
|
||||
let mut anomalies = detect_common_anomalies(§ions, entry_point, is_stripped);
|
||||
detect_pe_anomalies(
|
||||
&mut anomalies,
|
||||
pe,
|
||||
|
|
@ -213,15 +174,9 @@ fn map_architecture(machine: u16) -> Architecture {
|
|||
match machine {
|
||||
COFF_MACHINE_I386 => Architecture::X86,
|
||||
COFF_MACHINE_AMD64 => Architecture::X86_64,
|
||||
COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => {
|
||||
Architecture::Arm
|
||||
}
|
||||
COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => Architecture::Arm,
|
||||
COFF_MACHINE_ARM64 => Architecture::Aarch64,
|
||||
other => {
|
||||
Architecture::Other(format!(
|
||||
"pe-machine-{other:#x}"
|
||||
))
|
||||
}
|
||||
other => 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_CUI => "Console".into(),
|
||||
IMAGE_SUBSYSTEM_POSIX_CUI => "POSIX".into(),
|
||||
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => {
|
||||
"Windows CE".into()
|
||||
}
|
||||
IMAGE_SUBSYSTEM_EFI_APPLICATION => {
|
||||
"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_WINDOWS_CE_GUI => "Windows CE".into(),
|
||||
IMAGE_SUBSYSTEM_EFI_APPLICATION => "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(),
|
||||
other => format!("Unknown({other})"),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sections(
|
||||
pe: &PE,
|
||||
data: &[u8],
|
||||
) -> Vec<SectionInfo> {
|
||||
fn build_sections(pe: &PE, data: &[u8]) -> Vec<SectionInfo> {
|
||||
pe.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let name = section
|
||||
.name()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let raw_offset =
|
||||
section.pointer_to_raw_data as u64;
|
||||
let raw_size =
|
||||
section.size_of_raw_data as u64;
|
||||
let name = section.name().unwrap_or_default().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 permissions = SectionPermissions {
|
||||
read: (chars & IMAGE_SCN_MEM_READ) != 0,
|
||||
write: (chars & IMAGE_SCN_MEM_WRITE) != 0,
|
||||
execute: (chars & IMAGE_SCN_MEM_EXECUTE)
|
||||
!= 0,
|
||||
execute: (chars & IMAGE_SCN_MEM_EXECUTE) != 0,
|
||||
};
|
||||
let sha256 = compute_section_hash(
|
||||
data, raw_offset, raw_size,
|
||||
);
|
||||
let sha256 = compute_section_hash(data, raw_offset, raw_size);
|
||||
|
||||
SectionInfo {
|
||||
name,
|
||||
virtual_address: section.virtual_address
|
||||
as u64,
|
||||
virtual_address: section.virtual_address as u64,
|
||||
virtual_size: section.virtual_size as u64,
|
||||
raw_offset,
|
||||
raw_size,
|
||||
|
|
@ -293,10 +228,7 @@ fn build_segments(_pe: &PE) -> Vec<SegmentInfo> {
|
|||
Vec::new()
|
||||
}
|
||||
|
||||
fn detect_rich_header(
|
||||
data: &[u8],
|
||||
pe_offset: usize,
|
||||
) -> bool {
|
||||
fn detect_rich_header(data: &[u8], pe_offset: usize) -> bool {
|
||||
let end = pe_offset.min(data.len());
|
||||
data[..end]
|
||||
.windows(RICH_SIGNATURE.len())
|
||||
|
|
@ -313,34 +245,24 @@ fn detect_pe_anomalies(
|
|||
file_size: u64,
|
||||
) {
|
||||
if timestamp == 0 {
|
||||
anomalies.push(
|
||||
FormatAnomaly::SuspiciousTimestamp {
|
||||
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
|
||||
value: timestamp,
|
||||
reason: "zeroed timestamp".into(),
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if timestamp < PE_TIMESTAMP_MIN_VALID {
|
||||
anomalies.push(
|
||||
FormatAnomaly::SuspiciousTimestamp {
|
||||
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
|
||||
value: timestamp,
|
||||
reason: "timestamp before 1990".into(),
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if timestamp > PE_TIMESTAMP_MAX_VALID {
|
||||
anomalies.push(
|
||||
FormatAnomaly::SuspiciousTimestamp {
|
||||
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
|
||||
value: timestamp,
|
||||
reason: "timestamp after 2100".into(),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if has_tls {
|
||||
anomalies.push(
|
||||
FormatAnomaly::TlsCallbacksPresent {
|
||||
count: 1,
|
||||
},
|
||||
);
|
||||
anomalies.push(FormatAnomaly::TlsCallbacksPresent { count: 1 });
|
||||
}
|
||||
|
||||
if pe.imports.is_empty() {
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ pub struct AnalysisEngine {
|
|||
|
||||
impl AnalysisEngine {
|
||||
pub fn new() -> Result<Self, EngineError> {
|
||||
let passes: Vec<Box<dyn pass::AnalysisPass>> =
|
||||
vec![
|
||||
let passes: Vec<Box<dyn pass::AnalysisPass>> = vec![
|
||||
Box::new(FormatPass),
|
||||
Box::new(ImportPass),
|
||||
Box::new(StringPass),
|
||||
|
|
@ -67,23 +66,16 @@ impl AnalysisEngine {
|
|||
Ok(Self { pass_manager })
|
||||
}
|
||||
|
||||
pub fn analyze(
|
||||
&self,
|
||||
data: &[u8],
|
||||
file_name: &str,
|
||||
) -> (AnalysisContext, PassReport) {
|
||||
pub fn analyze(&self, data: &[u8], file_name: &str) -> (AnalysisContext, PassReport) {
|
||||
let sha256 = compute_sha256(data);
|
||||
let file_size = data.len() as u64;
|
||||
let mut ctx = AnalysisContext::new(
|
||||
BinarySource::Buffered(Arc::from(
|
||||
data.to_vec(),
|
||||
)),
|
||||
BinarySource::Buffered(Arc::from(data.to_vec())),
|
||||
sha256,
|
||||
file_name.to_string(),
|
||||
file_size,
|
||||
);
|
||||
let report =
|
||||
self.pass_manager.run_all(&mut ctx);
|
||||
let report = self.pass_manager.run_all(&mut ctx);
|
||||
(ctx, report)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,7 @@ mod private {
|
|||
pub trait AnalysisPass: private::Sealed + Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn dependencies(&self) -> &[&'static str];
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError>;
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -58,10 +55,7 @@ impl PassReport {
|
|||
}
|
||||
|
||||
pub fn failed_passes(&self) -> Vec<&PassOutcome> {
|
||||
self.outcomes
|
||||
.iter()
|
||||
.filter(|o| !o.success)
|
||||
.collect()
|
||||
self.outcomes.iter().filter(|o| !o.success).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,33 +65,23 @@ pub struct PassManager {
|
|||
}
|
||||
|
||||
impl PassManager {
|
||||
pub fn new(
|
||||
passes: Vec<Box<dyn AnalysisPass>>,
|
||||
) -> Self {
|
||||
pub fn new(passes: Vec<Box<dyn AnalysisPass>>) -> Self {
|
||||
let order = topological_order(&passes);
|
||||
Self { passes, order }
|
||||
}
|
||||
|
||||
pub fn run_all(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> PassReport {
|
||||
pub fn run_all(&self, ctx: &mut AnalysisContext) -> PassReport {
|
||||
let mut outcomes = Vec::with_capacity(self.passes.len());
|
||||
|
||||
for &idx in &self.order {
|
||||
let pass = &self.passes[idx];
|
||||
let start = Instant::now();
|
||||
let result = pass.run(ctx);
|
||||
let duration_ms =
|
||||
start.elapsed().as_millis() as u64;
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
pass = pass.name(),
|
||||
duration_ms,
|
||||
"pass completed"
|
||||
);
|
||||
tracing::info!(pass = pass.name(), duration_ms, "pass completed");
|
||||
PassOutcome {
|
||||
name: pass.name(),
|
||||
success: true,
|
||||
|
|
@ -128,9 +112,7 @@ impl PassManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn topological_order(
|
||||
passes: &[Box<dyn AnalysisPass>],
|
||||
) -> Vec<usize> {
|
||||
fn topological_order(passes: &[Box<dyn AnalysisPass>]) -> Vec<usize> {
|
||||
let name_to_idx: HashMap<&str, usize> = passes
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
|
@ -143,8 +125,7 @@ fn topological_order(
|
|||
|
||||
for (idx, pass) in passes.iter().enumerate() {
|
||||
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);
|
||||
in_degree[idx] += 1;
|
||||
}
|
||||
|
|
@ -204,10 +185,7 @@ mod tests {
|
|||
&self.deps
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
fn run(&self, _ctx: &mut AnalysisContext) -> Result<(), EngineError> {
|
||||
self.log.lock().unwrap().push(self.name);
|
||||
if self.should_fail {
|
||||
return Err(EngineError::PassFailed {
|
||||
|
|
@ -221,9 +199,7 @@ mod tests {
|
|||
|
||||
fn make_ctx() -> AnalysisContext {
|
||||
AnalysisContext::new(
|
||||
crate::context::BinarySource::Buffered(
|
||||
Arc::from(vec![0u8; 4]),
|
||||
),
|
||||
crate::context::BinarySource::Buffered(Arc::from(vec![0u8; 4])),
|
||||
"deadbeef".into(),
|
||||
"test.bin".into(),
|
||||
4,
|
||||
|
|
@ -295,16 +271,10 @@ mod tests {
|
|||
let report = manager.run_all(&mut ctx);
|
||||
|
||||
let execution_order = log.lock().unwrap().clone();
|
||||
assert_eq!(
|
||||
execution_order,
|
||||
vec!["first", "second", "third"]
|
||||
);
|
||||
assert_eq!(execution_order, vec!["first", "second", "third"]);
|
||||
assert!(!report.all_succeeded());
|
||||
assert_eq!(report.failed_passes().len(), 1);
|
||||
assert_eq!(
|
||||
report.failed_passes()[0].name,
|
||||
"second"
|
||||
);
|
||||
assert_eq!(report.failed_passes()[0].name, "second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -343,14 +313,10 @@ mod tests {
|
|||
manager.run_all(&mut ctx);
|
||||
|
||||
let order = log.lock().unwrap().clone();
|
||||
let format_pos =
|
||||
order.iter().position(|&n| n == "format").unwrap();
|
||||
let imports_pos =
|
||||
order.iter().position(|&n| n == "imports").unwrap();
|
||||
let entropy_pos =
|
||||
order.iter().position(|&n| n == "entropy").unwrap();
|
||||
let score_pos =
|
||||
order.iter().position(|&n| n == "score").unwrap();
|
||||
let format_pos = order.iter().position(|&n| n == "format").unwrap();
|
||||
let imports_pos = order.iter().position(|&n| n == "imports").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 < entropy_pos);
|
||||
|
|
@ -385,14 +351,12 @@ mod tests {
|
|||
fn reports_duration() {
|
||||
let log = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let passes: Vec<Box<dyn AnalysisPass>> = vec![
|
||||
Box::new(MockPass {
|
||||
let passes: Vec<Box<dyn AnalysisPass>> = vec![Box::new(MockPass {
|
||||
name: "fast",
|
||||
deps: vec![],
|
||||
log: Arc::clone(&log),
|
||||
should_fail: false,
|
||||
}),
|
||||
];
|
||||
})];
|
||||
|
||||
let manager = PassManager::new(passes);
|
||||
let mut ctx = make_ctx();
|
||||
|
|
|
|||
|
|
@ -41,23 +41,16 @@
|
|||
// FlowControlType
|
||||
// error.rs - EngineError
|
||||
|
||||
use std::collections::{
|
||||
BTreeMap, HashMap, HashSet, VecDeque,
|
||||
};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
|
||||
use iced_x86::{
|
||||
Decoder, DecoderOptions, FlowControl, Formatter,
|
||||
Instruction, IntelFormatter,
|
||||
};
|
||||
use iced_x86::{Decoder, DecoderOptions, FlowControl, Formatter, Instruction, IntelFormatter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::AnalysisContext;
|
||||
use crate::error::EngineError;
|
||||
use crate::formats::SectionInfo;
|
||||
use crate::pass::{AnalysisPass, Sealed};
|
||||
use crate::types::{
|
||||
Architecture, CfgEdgeType, FlowControlType,
|
||||
};
|
||||
use crate::types::{Architecture, CfgEdgeType, FlowControlType};
|
||||
|
||||
const MAX_FUNCTIONS: usize = 1000;
|
||||
const MAX_INSTRUCTIONS: usize = 50_000;
|
||||
|
|
@ -103,9 +96,7 @@ pub struct InstructionInfo {
|
|||
pub flow_control: FlowControlType,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Default, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct FunctionCfg {
|
||||
pub nodes: Vec<CfgNode>,
|
||||
pub edges: Vec<CfgEdge>,
|
||||
|
|
@ -139,12 +130,9 @@ impl AnalysisPass for DisasmPass {
|
|||
&["format"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
let format_result = ctx
|
||||
.format_result
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
|
||||
let format_result =
|
||||
ctx.format_result
|
||||
.as_ref()
|
||||
.ok_or_else(|| EngineError::MissingDependency {
|
||||
pass: "disasm".into(),
|
||||
|
|
@ -157,10 +145,7 @@ impl AnalysisPass for DisasmPass {
|
|||
Architecture::X86_64 => 64,
|
||||
_ => {
|
||||
ctx.disassembly_result =
|
||||
Some(empty_result(
|
||||
format_result.bits,
|
||||
format_result.entry_point,
|
||||
));
|
||||
Some(empty_result(format_result.bits, format_result.entry_point));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
|
@ -170,26 +155,15 @@ impl AnalysisPass for DisasmPass {
|
|||
let entry_point = format_result.entry_point;
|
||||
|
||||
let mut seeds = vec![entry_point];
|
||||
seeds.extend_from_slice(
|
||||
&format_result.function_hints,
|
||||
);
|
||||
seeds.extend_from_slice(&format_result.function_hints);
|
||||
|
||||
let result = disassemble(
|
||||
data,
|
||||
sections,
|
||||
bits,
|
||||
entry_point,
|
||||
&seeds,
|
||||
);
|
||||
let result = disassemble(data, sections, bits, entry_point, &seeds);
|
||||
ctx.disassembly_result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_result(
|
||||
bits: u8,
|
||||
entry_point: u64,
|
||||
) -> DisassemblyResult {
|
||||
fn empty_result(bits: u8, entry_point: u64) -> DisassemblyResult {
|
||||
DisassemblyResult {
|
||||
functions: Vec::new(),
|
||||
total_instructions: 0,
|
||||
|
|
@ -214,26 +188,20 @@ fn disassemble(
|
|||
let mut functions = Vec::new();
|
||||
let mut visited_functions = HashSet::new();
|
||||
let mut total_instructions = 0;
|
||||
let mut function_queue: VecDeque<u64> =
|
||||
seeds.iter().copied().collect();
|
||||
let mut function_queue: VecDeque<u64> = seeds.iter().copied().collect();
|
||||
|
||||
while let Some(func_addr) = function_queue.pop_front()
|
||||
{
|
||||
if functions.len() >= MAX_FUNCTIONS
|
||||
|| total_instructions >= MAX_INSTRUCTIONS
|
||||
{
|
||||
while let Some(func_addr) = function_queue.pop_front() {
|
||||
if functions.len() >= MAX_FUNCTIONS || total_instructions >= MAX_INSTRUCTIONS {
|
||||
break;
|
||||
}
|
||||
if !visited_functions.insert(func_addr) {
|
||||
continue;
|
||||
}
|
||||
if vaddr_to_offset(sections, func_addr).is_none()
|
||||
{
|
||||
if vaddr_to_offset(sections, func_addr).is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (func_info, discovered_calls) =
|
||||
disassemble_function(
|
||||
let (func_info, discovered_calls) = disassemble_function(
|
||||
data,
|
||||
sections,
|
||||
&exec_sections,
|
||||
|
|
@ -273,8 +241,7 @@ fn disassemble_function(
|
|||
is_entry_point: bool,
|
||||
instruction_budget: usize,
|
||||
) -> (FunctionInfo, Vec<u64>) {
|
||||
let mut decoded: BTreeMap<u64, DecodedInstruction> =
|
||||
BTreeMap::new();
|
||||
let mut decoded: BTreeMap<u64, DecodedInstruction> = BTreeMap::new();
|
||||
let mut block_leaders: HashSet<u64> = HashSet::new();
|
||||
let mut worklist: VecDeque<u64> = VecDeque::new();
|
||||
let mut visited: HashSet<u64> = HashSet::new();
|
||||
|
|
@ -292,10 +259,7 @@ fn disassemble_function(
|
|||
break;
|
||||
}
|
||||
|
||||
let offset = match vaddr_to_offset(
|
||||
all_sections,
|
||||
addr,
|
||||
) {
|
||||
let offset = match vaddr_to_offset(all_sections, addr) {
|
||||
Some(o) => o as usize,
|
||||
None => continue,
|
||||
};
|
||||
|
|
@ -310,17 +274,10 @@ fn disassemble_function(
|
|||
}
|
||||
|
||||
let slice = &data[offset..];
|
||||
let mut decoder = Decoder::with_ip(
|
||||
bits,
|
||||
slice,
|
||||
addr,
|
||||
DecoderOptions::NONE,
|
||||
);
|
||||
let mut decoder = Decoder::with_ip(bits, slice, addr, DecoderOptions::NONE);
|
||||
let mut instr = Instruction::default();
|
||||
|
||||
while decoder.can_decode()
|
||||
&& decoded.len() < instruction_budget
|
||||
{
|
||||
while decoder.can_decode() && decoded.len() < instruction_budget {
|
||||
decoder.decode_out(&mut instr);
|
||||
let ip = instr.ip();
|
||||
|
||||
|
|
@ -328,30 +285,21 @@ fn disassemble_function(
|
|||
break;
|
||||
}
|
||||
|
||||
if ip != addr
|
||||
&& block_leaders.contains(&ip)
|
||||
{
|
||||
if ip != addr && block_leaders.contains(&ip) {
|
||||
break;
|
||||
}
|
||||
|
||||
let fc = instr.flow_control();
|
||||
let mnemonic = format!("{:?}", instr.mnemonic())
|
||||
.to_ascii_lowercase();
|
||||
let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
|
||||
|
||||
let mut operands_str = String::new();
|
||||
formatter
|
||||
.format(&instr, &mut operands_str);
|
||||
formatter.format(&instr, &mut operands_str);
|
||||
let operands = operands_str
|
||||
.split_once(' ')
|
||||
.map_or(String::new(), |(_, ops)| {
|
||||
ops.to_string()
|
||||
});
|
||||
.map_or(String::new(), |(_, ops)| ops.to_string());
|
||||
|
||||
let instr_bytes = &data
|
||||
[offset + (ip - addr) as usize
|
||||
..offset
|
||||
+ (ip - addr) as usize
|
||||
+ instr.len()];
|
||||
let instr_bytes =
|
||||
&data[offset + (ip - addr) as usize..offset + (ip - addr) as usize + instr.len()];
|
||||
|
||||
let flow_type = map_flow_control(fc);
|
||||
|
||||
|
|
@ -374,13 +322,10 @@ fn disassemble_function(
|
|||
|
||||
match fc {
|
||||
FlowControl::ConditionalBranch => {
|
||||
let target =
|
||||
instr.near_branch_target();
|
||||
let target = instr.near_branch_target();
|
||||
let fall = instr.next_ip();
|
||||
|
||||
if let Some(di) =
|
||||
decoded.get_mut(&ip)
|
||||
{
|
||||
if let Some(di) = decoded.get_mut(&ip) {
|
||||
di.branch_target = Some(target);
|
||||
di.fallthrough = Some(fall);
|
||||
}
|
||||
|
|
@ -392,11 +337,8 @@ fn disassemble_function(
|
|||
break;
|
||||
}
|
||||
FlowControl::UnconditionalBranch => {
|
||||
let target =
|
||||
instr.near_branch_target();
|
||||
if let Some(di) =
|
||||
decoded.get_mut(&ip)
|
||||
{
|
||||
let target = instr.near_branch_target();
|
||||
if let Some(di) = decoded.get_mut(&ip) {
|
||||
di.branch_target = Some(target);
|
||||
}
|
||||
block_leaders.insert(target);
|
||||
|
|
@ -410,36 +352,24 @@ fn disassemble_function(
|
|||
break;
|
||||
}
|
||||
FlowControl::Call => {
|
||||
let target =
|
||||
instr.near_branch_target();
|
||||
let target = instr.near_branch_target();
|
||||
if target != 0 {
|
||||
discovered_calls.push(target);
|
||||
}
|
||||
}
|
||||
FlowControl::IndirectCall => {}
|
||||
FlowControl::Next
|
||||
| FlowControl::XbeginXabortXend => {}
|
||||
FlowControl::Next | FlowControl::XbeginXabortXend => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let basic_blocks = build_basic_blocks(
|
||||
&decoded,
|
||||
&block_leaders,
|
||||
);
|
||||
let instruction_count: usize = basic_blocks
|
||||
.iter()
|
||||
.map(|bb| bb.instruction_count)
|
||||
.sum();
|
||||
let basic_blocks = build_basic_blocks(&decoded, &block_leaders);
|
||||
let instruction_count: usize = basic_blocks.iter().map(|bb| bb.instruction_count).sum();
|
||||
|
||||
let size = if let (Some(first), Some(last)) = (
|
||||
decoded.keys().next(),
|
||||
decoded.keys().next_back(),
|
||||
) {
|
||||
let size =
|
||||
if let (Some(first), Some(last)) = (decoded.keys().next(), decoded.keys().next_back()) {
|
||||
if let Some(last_instr) = decoded.get(last) {
|
||||
last_instr.info.address
|
||||
+ last_instr.info.size as u64
|
||||
- first
|
||||
last_instr.info.address + last_instr.info.size as u64 - first
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -447,8 +377,7 @@ fn disassemble_function(
|
|||
0
|
||||
};
|
||||
|
||||
let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT
|
||||
{
|
||||
let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT {
|
||||
build_cfg(&basic_blocks)
|
||||
} else {
|
||||
FunctionCfg::default()
|
||||
|
|
@ -483,14 +412,11 @@ fn build_basic_blocks(
|
|||
}
|
||||
|
||||
let mut blocks: Vec<BasicBlockInfo> = Vec::new();
|
||||
let mut current_instrs: Vec<InstructionInfo> =
|
||||
Vec::new();
|
||||
let mut current_instrs: Vec<InstructionInfo> = Vec::new();
|
||||
let mut block_start: Option<u64> = None;
|
||||
|
||||
for (&addr, di) in decoded {
|
||||
if leaders.contains(&addr)
|
||||
&& !current_instrs.is_empty()
|
||||
{
|
||||
if leaders.contains(&addr) && !current_instrs.is_empty() {
|
||||
let bb = finalize_block(
|
||||
¤t_instrs,
|
||||
block_start.unwrap_or(addr),
|
||||
|
|
@ -527,35 +453,24 @@ fn build_basic_blocks(
|
|||
}
|
||||
}
|
||||
|
||||
if !current_instrs.is_empty() {
|
||||
if let Some(start) = block_start {
|
||||
let bb = finalize_block(
|
||||
¤t_instrs,
|
||||
start,
|
||||
decoded,
|
||||
leaders,
|
||||
);
|
||||
if !current_instrs.is_empty()
|
||||
&& let Some(start) = block_start
|
||||
{
|
||||
let bb = finalize_block(¤t_instrs, start, decoded, leaders);
|
||||
blocks.push(bb);
|
||||
}
|
||||
}
|
||||
|
||||
let block_starts: HashSet<u64> =
|
||||
blocks.iter().map(|b| b.start_address).collect();
|
||||
let block_starts: HashSet<u64> = blocks.iter().map(|b| b.start_address).collect();
|
||||
|
||||
for block in &mut blocks {
|
||||
block
|
||||
.successors
|
||||
.retain(|s| block_starts.contains(s));
|
||||
block.successors.retain(|s| block_starts.contains(s));
|
||||
}
|
||||
|
||||
let predecessor_map: HashMap<u64, Vec<u64>> = {
|
||||
let mut map: HashMap<u64, Vec<u64>> =
|
||||
HashMap::new();
|
||||
let mut map: HashMap<u64, Vec<u64>> = HashMap::new();
|
||||
for block in &blocks {
|
||||
for &succ in &block.successors {
|
||||
map.entry(succ)
|
||||
.or_default()
|
||||
.push(block.start_address);
|
||||
map.entry(succ).or_default().push(block.start_address);
|
||||
}
|
||||
}
|
||||
map
|
||||
|
|
@ -578,8 +493,7 @@ fn finalize_block(
|
|||
leaders: &HashSet<u64>,
|
||||
) -> BasicBlockInfo {
|
||||
let last = instructions.last().unwrap();
|
||||
let end_address =
|
||||
last.address + last.size as u64 - 1;
|
||||
let end_address = last.address + last.size as u64 - 1;
|
||||
|
||||
let mut successors = Vec::new();
|
||||
let last_addr = last.address;
|
||||
|
|
@ -591,14 +505,10 @@ fn finalize_block(
|
|||
successors.push(fall);
|
||||
} else if !matches!(
|
||||
di.info.flow_control,
|
||||
FlowControlType::Branch
|
||||
| FlowControlType::Return
|
||||
| FlowControlType::Interrupt
|
||||
FlowControlType::Branch | FlowControlType::Return | FlowControlType::Interrupt
|
||||
) {
|
||||
let next = di.next_ip;
|
||||
if leaders.contains(&next)
|
||||
|| decoded.contains_key(&next)
|
||||
{
|
||||
if leaders.contains(&next) || decoded.contains_key(&next) {
|
||||
successors.push(next);
|
||||
}
|
||||
}
|
||||
|
|
@ -614,9 +524,7 @@ fn finalize_block(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_cfg(
|
||||
blocks: &[BasicBlockInfo],
|
||||
) -> FunctionCfg {
|
||||
fn build_cfg(blocks: &[BasicBlockInfo]) -> FunctionCfg {
|
||||
let mut nodes = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
|
||||
|
|
@ -635,35 +543,23 @@ fn build_cfg(
|
|||
|
||||
nodes.push(CfgNode {
|
||||
id: block.start_address,
|
||||
label: format!(
|
||||
"0x{:x}",
|
||||
block.start_address
|
||||
),
|
||||
label: format!("0x{:x}", block.start_address),
|
||||
instruction_count: block.instruction_count,
|
||||
instructions_preview: preview,
|
||||
});
|
||||
|
||||
let last_instr = block.instructions.last();
|
||||
for &succ in &block.successors {
|
||||
let edge_type =
|
||||
if let Some(last) = last_instr {
|
||||
let edge_type = if let Some(last) = last_instr {
|
||||
match last.flow_control {
|
||||
FlowControlType::ConditionalBranch => {
|
||||
if succ
|
||||
== block
|
||||
.successors
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
{
|
||||
if succ == block.successors.first().copied().unwrap_or(0) {
|
||||
CfgEdgeType::ConditionalTrue
|
||||
} else {
|
||||
CfgEdgeType::ConditionalFalse
|
||||
}
|
||||
}
|
||||
FlowControlType::Branch => {
|
||||
CfgEdgeType::Unconditional
|
||||
}
|
||||
FlowControlType::Branch => CfgEdgeType::Unconditional,
|
||||
_ => CfgEdgeType::Fallthrough,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -681,99 +577,54 @@ fn build_cfg(
|
|||
FunctionCfg { nodes, edges }
|
||||
}
|
||||
|
||||
fn vaddr_to_offset(
|
||||
sections: &[SectionInfo],
|
||||
vaddr: u64,
|
||||
) -> Option<u64> {
|
||||
fn vaddr_to_offset(sections: &[SectionInfo], vaddr: u64) -> Option<u64> {
|
||||
sections.iter().find_map(|s| {
|
||||
if s.raw_size > 0
|
||||
&& vaddr >= s.virtual_address
|
||||
&& vaddr
|
||||
< s.virtual_address + s.virtual_size
|
||||
&& vaddr < s.virtual_address + s.virtual_size
|
||||
{
|
||||
Some(
|
||||
s.raw_offset
|
||||
+ (vaddr - s.virtual_address),
|
||||
)
|
||||
Some(s.raw_offset + (vaddr - s.virtual_address))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_in_exec_section(
|
||||
exec_sections: &[&SectionInfo],
|
||||
vaddr: u64,
|
||||
) -> bool {
|
||||
exec_sections.iter().any(|s| {
|
||||
vaddr >= s.virtual_address
|
||||
&& vaddr
|
||||
< s.virtual_address + s.virtual_size
|
||||
})
|
||||
fn is_in_exec_section(exec_sections: &[&SectionInfo], vaddr: u64) -> bool {
|
||||
exec_sections
|
||||
.iter()
|
||||
.any(|s| vaddr >= s.virtual_address && vaddr < s.virtual_address + s.virtual_size)
|
||||
}
|
||||
|
||||
fn map_flow_control(
|
||||
fc: FlowControl,
|
||||
) -> FlowControlType {
|
||||
fn map_flow_control(fc: FlowControl) -> FlowControlType {
|
||||
match fc {
|
||||
FlowControl::Next
|
||||
| FlowControl::XbeginXabortXend => {
|
||||
FlowControlType::Next
|
||||
}
|
||||
FlowControl::UnconditionalBranch
|
||||
| FlowControl::IndirectBranch => {
|
||||
FlowControlType::Branch
|
||||
}
|
||||
FlowControl::ConditionalBranch => {
|
||||
FlowControlType::ConditionalBranch
|
||||
}
|
||||
FlowControl::Call
|
||||
| FlowControl::IndirectCall => {
|
||||
FlowControlType::Call
|
||||
}
|
||||
FlowControl::Next | FlowControl::XbeginXabortXend => FlowControlType::Next,
|
||||
FlowControl::UnconditionalBranch | FlowControl::IndirectBranch => FlowControlType::Branch,
|
||||
FlowControl::ConditionalBranch => FlowControlType::ConditionalBranch,
|
||||
FlowControl::Call | FlowControl::IndirectCall => FlowControlType::Call,
|
||||
FlowControl::Return => FlowControlType::Return,
|
||||
FlowControl::Interrupt
|
||||
| FlowControl::Exception => {
|
||||
FlowControlType::Interrupt
|
||||
}
|
||||
FlowControl::Interrupt | FlowControl::Exception => FlowControlType::Interrupt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disassemble_code(
|
||||
code: &[u8],
|
||||
base_addr: u64,
|
||||
bits: u32,
|
||||
) -> Vec<InstructionInfo> {
|
||||
let mut decoder = Decoder::with_ip(
|
||||
bits,
|
||||
code,
|
||||
base_addr,
|
||||
DecoderOptions::NONE,
|
||||
);
|
||||
pub fn disassemble_code(code: &[u8], 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 instr = Instruction::default();
|
||||
let mut result = Vec::new();
|
||||
|
||||
while decoder.can_decode() {
|
||||
decoder.decode_out(&mut instr);
|
||||
let mnemonic = format!(
|
||||
"{:?}",
|
||||
instr.mnemonic()
|
||||
)
|
||||
.to_ascii_lowercase();
|
||||
let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
|
||||
|
||||
let mut full = String::new();
|
||||
formatter.format(&instr, &mut full);
|
||||
let operands = full
|
||||
.split_once(' ')
|
||||
.map_or(String::new(), |(_, ops)| {
|
||||
ops.to_string()
|
||||
});
|
||||
.map_or(String::new(), |(_, ops)| ops.to_string());
|
||||
|
||||
let start =
|
||||
(instr.ip() - base_addr) as usize;
|
||||
let bytes =
|
||||
code[start..start + instr.len()].to_vec();
|
||||
let start = (instr.ip() - base_addr) as usize;
|
||||
let bytes = code[start..start + instr.len()].to_vec();
|
||||
|
||||
result.push(InstructionInfo {
|
||||
address: instr.ip(),
|
||||
|
|
@ -781,9 +632,7 @@ pub fn disassemble_code(
|
|||
mnemonic,
|
||||
operands,
|
||||
size: instr.len() as u8,
|
||||
flow_control: map_flow_control(
|
||||
instr.flow_control(),
|
||||
),
|
||||
flow_control: map_flow_control(instr.flow_control()),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -799,13 +648,8 @@ mod tests {
|
|||
use crate::types::SectionPermissions;
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
|
||||
|
|
@ -820,27 +664,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn disassemble_simple_function() {
|
||||
let code: &[u8] = &[
|
||||
0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0,
|
||||
0x5D, 0xC3,
|
||||
];
|
||||
let instrs =
|
||||
disassemble_code(code, 0x1000, 64);
|
||||
let code: &[u8] = &[0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, 0x5D, 0xC3];
|
||||
let instrs = disassemble_code(code, 0x1000, 64);
|
||||
assert_eq!(instrs.len(), 5);
|
||||
assert_eq!(instrs[0].mnemonic, "push");
|
||||
assert_eq!(instrs[4].mnemonic, "ret");
|
||||
assert_eq!(
|
||||
instrs[4].flow_control,
|
||||
FlowControlType::Return
|
||||
);
|
||||
assert_eq!(instrs[4].flow_control, FlowControlType::Return);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_block_split_on_branch() {
|
||||
let code: &[u8] = &[
|
||||
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
|
||||
0x31, 0xC9, 0xC3,
|
||||
];
|
||||
let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
|
||||
|
||||
let sections = vec![SectionInfo {
|
||||
name: ".text".into(),
|
||||
|
|
@ -856,13 +690,7 @@ mod tests {
|
|||
sha256: String::new(),
|
||||
}];
|
||||
|
||||
let result = disassemble(
|
||||
code,
|
||||
§ions,
|
||||
64,
|
||||
0x1000,
|
||||
&[0x1000],
|
||||
);
|
||||
let result = disassemble(code, §ions, 64, 0x1000, &[0x1000]);
|
||||
assert!(!result.functions.is_empty());
|
||||
let func = &result.functions[0];
|
||||
assert!(
|
||||
|
|
@ -874,10 +702,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cfg_edges_conditional() {
|
||||
let code: &[u8] = &[
|
||||
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
|
||||
0x31, 0xC9, 0xC3,
|
||||
];
|
||||
let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
|
||||
|
||||
let sections = vec![SectionInfo {
|
||||
name: ".text".into(),
|
||||
|
|
@ -893,35 +718,21 @@ mod tests {
|
|||
sha256: String::new(),
|
||||
}];
|
||||
|
||||
let result = disassemble(
|
||||
code,
|
||||
§ions,
|
||||
64,
|
||||
0x1000,
|
||||
&[0x1000],
|
||||
);
|
||||
let result = disassemble(code, §ions, 64, 0x1000, &[0x1000]);
|
||||
let func = &result.functions[0];
|
||||
assert!(
|
||||
!func.cfg.edges.is_empty(),
|
||||
"CFG should have edges"
|
||||
);
|
||||
assert!(
|
||||
!func.cfg.nodes.is_empty(),
|
||||
"CFG should have nodes"
|
||||
);
|
||||
assert!(!func.cfg.edges.is_empty(), "CFG should have edges");
|
||||
assert!(!func.cfg.nodes.is_empty(), "CFG should have nodes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_x86_returns_empty() {
|
||||
let data = vec![0u8; 64];
|
||||
let mut ctx = make_ctx(data);
|
||||
ctx.format_result =
|
||||
Some(crate::formats::FormatResult {
|
||||
ctx.format_result = Some(crate::formats::FormatResult {
|
||||
format: crate::types::BinaryFormat::Elf,
|
||||
architecture: Architecture::Aarch64,
|
||||
bits: 64,
|
||||
endianness:
|
||||
crate::types::Endianness::Little,
|
||||
endianness: crate::types::Endianness::Little,
|
||||
entry_point: 0x1000,
|
||||
is_stripped: false,
|
||||
is_pie: false,
|
||||
|
|
@ -946,28 +757,22 @@ mod tests {
|
|||
let data = load_fixture("hello_elf");
|
||||
let mut ctx = make_ctx(data);
|
||||
|
||||
crate::passes::format::FormatPass
|
||||
.run(&mut ctx)
|
||||
.unwrap();
|
||||
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
|
||||
DisasmPass.run(&mut ctx).unwrap();
|
||||
|
||||
let result =
|
||||
ctx.disassembly_result.as_ref().unwrap();
|
||||
let result = ctx.disassembly_result.as_ref().unwrap();
|
||||
assert!(
|
||||
result.total_functions > 0,
|
||||
"should find at least one function"
|
||||
);
|
||||
assert!(result.total_instructions > 0);
|
||||
|
||||
let entry_func = result.functions.iter().find(
|
||||
|f| {
|
||||
f.address
|
||||
== result.entry_function_address
|
||||
},
|
||||
);
|
||||
let entry_func = result
|
||||
.functions
|
||||
.iter()
|
||||
.find(|f| f.address == result.entry_function_address);
|
||||
assert!(
|
||||
entry_func.is_some()
|
||||
|| !result.functions.is_empty(),
|
||||
entry_func.is_some() || !result.functions.is_empty(),
|
||||
"should have disassembled functions"
|
||||
);
|
||||
}
|
||||
|
|
@ -977,9 +782,7 @@ mod tests {
|
|||
let data = load_fixture("hello_elf");
|
||||
let mut ctx = make_ctx(data);
|
||||
|
||||
crate::passes::format::FormatPass
|
||||
.run(&mut ctx)
|
||||
.unwrap();
|
||||
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
|
||||
assert!(ctx.disassembly_result.is_none());
|
||||
|
||||
DisasmPass.run(&mut ctx).unwrap();
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ use crate::context::AnalysisContext;
|
|||
use crate::error::EngineError;
|
||||
use crate::formats::SectionInfo;
|
||||
use crate::pass::{AnalysisPass, Sealed};
|
||||
use crate::types::{
|
||||
EntropyClassification, EntropyFlag,
|
||||
};
|
||||
use crate::types::{EntropyClassification, EntropyFlag};
|
||||
|
||||
const PLAINTEXT_MAX: f64 = 3.5;
|
||||
const NATIVE_CODE_MAX: f64 = 6.0;
|
||||
|
|
@ -120,12 +118,9 @@ impl AnalysisPass for EntropyPass {
|
|||
&["format"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
let format_result = ctx
|
||||
.format_result
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
|
||||
let format_result =
|
||||
ctx.format_result
|
||||
.as_ref()
|
||||
.ok_or_else(|| EngineError::MissingDependency {
|
||||
pass: "entropy".into(),
|
||||
|
|
@ -133,21 +128,13 @@ impl AnalysisPass for EntropyPass {
|
|||
})?;
|
||||
|
||||
let data = ctx.data();
|
||||
let result = analyze_entropy(
|
||||
data,
|
||||
&format_result.sections,
|
||||
format_result.entry_point,
|
||||
);
|
||||
let result = analyze_entropy(data, &format_result.sections, format_result.entry_point);
|
||||
ctx.entropy_result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_entropy(
|
||||
data: &[u8],
|
||||
sections: &[SectionInfo],
|
||||
entry_point: u64,
|
||||
) -> EntropyResult {
|
||||
fn analyze_entropy(data: &[u8], sections: &[SectionInfo], entry_point: u64) -> EntropyResult {
|
||||
let overall_entropy = shannon_entropy(data);
|
||||
let mut section_entropies = Vec::new();
|
||||
let mut packing_indicators = Vec::new();
|
||||
|
|
@ -155,11 +142,7 @@ fn analyze_entropy(
|
|||
let mut structural_count = 0;
|
||||
|
||||
for section in sections {
|
||||
let section_data = read_section_data(
|
||||
data,
|
||||
section.raw_offset,
|
||||
section.raw_size,
|
||||
);
|
||||
let section_data = read_section_data(data, section.raw_offset, section.raw_size);
|
||||
let entropy = if section_data.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
|
|
@ -167,8 +150,7 @@ fn analyze_entropy(
|
|||
};
|
||||
let classification = classify_entropy(entropy);
|
||||
let vr_ratio = if section.raw_size > 0 {
|
||||
section.virtual_size as f64
|
||||
/ section.raw_size as f64
|
||||
section.virtual_size as f64 / section.raw_size as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
|
@ -181,24 +163,18 @@ fn analyze_entropy(
|
|||
if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
|
||||
flags.push(EntropyFlag::HighVirtualToRawRatio);
|
||||
}
|
||||
if section.raw_size == 0
|
||||
&& section.virtual_size > 0
|
||||
{
|
||||
if section.raw_size == 0 && section.virtual_size > 0 {
|
||||
flags.push(EntropyFlag::EmptyRawData);
|
||||
}
|
||||
if section.permissions.is_rwx() {
|
||||
flags.push(EntropyFlag::Rwx);
|
||||
}
|
||||
|
||||
if let Some(packer) =
|
||||
detect_packer_by_section(§ion.name)
|
||||
{
|
||||
if let Some(packer) = detect_packer_by_section(§ion.name) {
|
||||
flags.push(EntropyFlag::PackerSectionName);
|
||||
packing_indicators.push(PackingIndicator {
|
||||
indicator_type: "section_name".into(),
|
||||
description: format!(
|
||||
"Section name matches {packer} packer"
|
||||
),
|
||||
description: format!("Section name matches {packer} packer"),
|
||||
evidence: section.name.clone(),
|
||||
packer_name: Some(packer.into()),
|
||||
});
|
||||
|
|
@ -207,15 +183,11 @@ fn analyze_entropy(
|
|||
}
|
||||
}
|
||||
|
||||
if section.raw_size == 0
|
||||
&& section.virtual_size > 0
|
||||
&& section.permissions.execute
|
||||
{
|
||||
if section.raw_size == 0 && section.virtual_size > 0 && section.permissions.execute {
|
||||
structural_count += 1;
|
||||
packing_indicators.push(PackingIndicator {
|
||||
indicator_type: "structural".into(),
|
||||
description:
|
||||
"Empty raw data with executable \
|
||||
description: "Empty raw data with executable \
|
||||
virtual section"
|
||||
.into(),
|
||||
evidence: format!(
|
||||
|
|
@ -230,13 +202,8 @@ fn analyze_entropy(
|
|||
structural_count += 1;
|
||||
packing_indicators.push(PackingIndicator {
|
||||
indicator_type: "structural".into(),
|
||||
description:
|
||||
"High virtual to raw size ratio"
|
||||
.into(),
|
||||
evidence: format!(
|
||||
"section={} ratio={vr_ratio:.1}",
|
||||
section.name
|
||||
),
|
||||
description: "High virtual to raw size ratio".into(),
|
||||
evidence: format!("section={} ratio={vr_ratio:.1}", section.name),
|
||||
packer_name: None,
|
||||
});
|
||||
}
|
||||
|
|
@ -254,38 +221,27 @@ fn analyze_entropy(
|
|||
});
|
||||
}
|
||||
|
||||
if let Some(ep_section) = find_ep_section(
|
||||
sections,
|
||||
entry_point,
|
||||
) {
|
||||
if let Some(ep_section) = find_ep_section(sections, entry_point) {
|
||||
let ep_file_offset = entry_point
|
||||
.wrapping_sub(ep_section.virtual_address)
|
||||
.wrapping_add(ep_section.raw_offset);
|
||||
if let Some(&first_byte) =
|
||||
data.get(ep_file_offset as usize)
|
||||
if let Some(&first_byte) = data.get(ep_file_offset as usize)
|
||||
&& first_byte == PUSHAD_OPCODE
|
||||
{
|
||||
if first_byte == PUSHAD_OPCODE {
|
||||
packing_indicators.push(
|
||||
PackingIndicator {
|
||||
indicator_type: "entry_point"
|
||||
.into(),
|
||||
description:
|
||||
"PUSHAD at entry point"
|
||||
.into(),
|
||||
packing_indicators.push(PackingIndicator {
|
||||
indicator_type: "entry_point".into(),
|
||||
description: "PUSHAD at entry point".into(),
|
||||
evidence: format!(
|
||||
"byte 0x{PUSHAD_OPCODE:02x} \
|
||||
at EP offset 0x{ep_file_offset:x}"
|
||||
),
|
||||
packer_name: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let packing_detected = packer_name.is_some()
|
||||
|| structural_count
|
||||
>= STRUCTURAL_INDICATORS_FOR_PACKING;
|
||||
let packing_detected =
|
||||
packer_name.is_some() || structural_count >= STRUCTURAL_INDICATORS_FOR_PACKING;
|
||||
|
||||
EntropyResult {
|
||||
overall_entropy,
|
||||
|
|
@ -314,9 +270,7 @@ fn shannon_entropy(data: &[u8]) -> f64 {
|
|||
.sum()
|
||||
}
|
||||
|
||||
fn classify_entropy(
|
||||
entropy: f64,
|
||||
) -> EntropyClassification {
|
||||
fn classify_entropy(entropy: f64) -> EntropyClassification {
|
||||
if entropy < PLAINTEXT_MAX {
|
||||
EntropyClassification::Plaintext
|
||||
} else if entropy < NATIVE_CODE_MAX {
|
||||
|
|
@ -330,20 +284,14 @@ fn classify_entropy(
|
|||
}
|
||||
}
|
||||
|
||||
fn detect_packer_by_section(
|
||||
name: &str,
|
||||
) -> Option<&'static str> {
|
||||
fn detect_packer_by_section(name: &str) -> Option<&'static str> {
|
||||
PACKER_SECTION_NAMES
|
||||
.iter()
|
||||
.find(|&&(section_name, _)| section_name == name)
|
||||
.map(|&(_, packer)| packer)
|
||||
}
|
||||
|
||||
fn read_section_data(
|
||||
data: &[u8],
|
||||
offset: u64,
|
||||
size: u64,
|
||||
) -> &[u8] {
|
||||
fn read_section_data(data: &[u8], offset: u64, size: u64) -> &[u8] {
|
||||
if size == 0 {
|
||||
return &[];
|
||||
}
|
||||
|
|
@ -355,14 +303,9 @@ fn read_section_data(
|
|||
&data[start..end]
|
||||
}
|
||||
|
||||
fn find_ep_section(
|
||||
sections: &[SectionInfo],
|
||||
entry_point: u64,
|
||||
) -> Option<&SectionInfo> {
|
||||
fn find_ep_section(sections: &[SectionInfo], entry_point: u64) -> Option<&SectionInfo> {
|
||||
sections.iter().find(|s| {
|
||||
entry_point >= s.virtual_address
|
||||
&& entry_point
|
||||
< s.virtual_address + s.virtual_size
|
||||
entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -375,13 +318,8 @@ mod tests {
|
|||
use crate::types::SectionPermissions;
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
|
||||
|
|
@ -402,8 +340,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn entropy_uniform_distribution() {
|
||||
let data: Vec<u8> =
|
||||
(0..=255u8).cycle().take(1024).collect();
|
||||
let data: Vec<u8> = (0..=255u8).cycle().take(1024).collect();
|
||||
let e = shannon_entropy(&data);
|
||||
assert!(
|
||||
(e - 8.0).abs() < 0.01,
|
||||
|
|
@ -421,50 +358,20 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn entropy_classification_thresholds() {
|
||||
assert_eq!(
|
||||
classify_entropy(2.0),
|
||||
EntropyClassification::Plaintext
|
||||
);
|
||||
assert_eq!(
|
||||
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
|
||||
);
|
||||
assert_eq!(classify_entropy(2.0), EntropyClassification::Plaintext);
|
||||
assert_eq!(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]
|
||||
fn packer_section_name_detection() {
|
||||
assert_eq!(
|
||||
detect_packer_by_section("UPX0"),
|
||||
Some("UPX")
|
||||
);
|
||||
assert_eq!(
|
||||
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
|
||||
);
|
||||
assert_eq!(detect_packer_by_section("UPX0"), Some("UPX"));
|
||||
assert_eq!(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]
|
||||
|
|
@ -483,17 +390,11 @@ mod tests {
|
|||
sha256: String::new(),
|
||||
}];
|
||||
|
||||
let data: Vec<u8> =
|
||||
(0..=255u8).cycle().take(256).collect();
|
||||
let result =
|
||||
analyze_entropy(&data, §ions, 0x1000);
|
||||
let data: Vec<u8> = (0..=255u8).cycle().take(256).collect();
|
||||
let result = analyze_entropy(&data, §ions, 0x1000);
|
||||
let text_section = &result.sections[0];
|
||||
assert!(
|
||||
text_section.entropy > HIGH_ENTROPY_THRESHOLD
|
||||
);
|
||||
assert!(text_section
|
||||
.flags
|
||||
.contains(&EntropyFlag::HighEntropy));
|
||||
assert!(text_section.entropy > HIGH_ENTROPY_THRESHOLD);
|
||||
assert!(text_section.flags.contains(&EntropyFlag::HighEntropy));
|
||||
assert!(text_section.is_anomalous);
|
||||
}
|
||||
|
||||
|
|
@ -529,27 +430,17 @@ mod tests {
|
|||
];
|
||||
|
||||
let data = vec![0u8; 0x4200];
|
||||
let result =
|
||||
analyze_entropy(&data, §ions, 0x11000);
|
||||
let result = analyze_entropy(&data, §ions, 0x11000);
|
||||
assert!(result.packing_detected);
|
||||
assert_eq!(
|
||||
result.packer_name,
|
||||
Some("UPX".into())
|
||||
);
|
||||
assert_eq!(result.packer_name, Some("UPX".into()));
|
||||
assert!(!result.packing_indicators.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elf_entropy_analysis() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let format_result =
|
||||
crate::formats::parse_format(&data)
|
||||
.unwrap();
|
||||
let result = analyze_entropy(
|
||||
&data,
|
||||
&format_result.sections,
|
||||
format_result.entry_point,
|
||||
);
|
||||
let format_result = crate::formats::parse_format(&data).unwrap();
|
||||
let result = analyze_entropy(&data, &format_result.sections, format_result.entry_point);
|
||||
|
||||
assert!(result.overall_entropy > 0.0);
|
||||
assert!(!result.sections.is_empty());
|
||||
|
|
@ -561,9 +452,7 @@ mod tests {
|
|||
let data = load_fixture("hello_elf");
|
||||
let mut ctx = make_ctx(data);
|
||||
|
||||
crate::passes::format::FormatPass
|
||||
.run(&mut ctx)
|
||||
.unwrap();
|
||||
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
|
||||
assert!(ctx.format_result.is_some());
|
||||
|
||||
EntropyPass.run(&mut ctx).unwrap();
|
||||
|
|
|
|||
|
|
@ -36,12 +36,8 @@ impl AnalysisPass for FormatPass {
|
|||
&[]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
let result =
|
||||
formats::parse_format(ctx.data())?;
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
|
||||
let result = formats::parse_format(ctx.data())?;
|
||||
ctx.format_result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -56,13 +52,8 @@ mod tests {
|
|||
use crate::types::{Architecture, BinaryFormat, Endianness};
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
|
||||
|
|
@ -78,19 +69,12 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_elf_basic_metadata() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
assert_eq!(result.format, BinaryFormat::Elf);
|
||||
assert_eq!(
|
||||
result.architecture,
|
||||
Architecture::X86_64
|
||||
);
|
||||
assert_eq!(result.architecture, Architecture::X86_64);
|
||||
assert_eq!(result.bits, 64);
|
||||
assert_eq!(
|
||||
result.endianness,
|
||||
Endianness::Little
|
||||
);
|
||||
assert_eq!(result.endianness, Endianness::Little);
|
||||
assert!(result.entry_point > 0);
|
||||
assert!(result.is_pie);
|
||||
assert!(!result.is_stripped);
|
||||
|
|
@ -99,14 +83,10 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_elf_sections_present() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
assert!(!result.sections.is_empty());
|
||||
let text = result
|
||||
.sections
|
||||
.iter()
|
||||
.find(|s| s.name == ".text");
|
||||
let text = result.sections.iter().find(|s| s.name == ".text");
|
||||
assert!(text.is_some());
|
||||
assert!(text.unwrap().permissions.execute);
|
||||
}
|
||||
|
|
@ -114,41 +94,35 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_elf_segments_present() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
assert!(!result.segments.is_empty());
|
||||
let load_segments: Vec<_> = result
|
||||
.segments
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
s.name.as_deref() == Some("LOAD")
|
||||
})
|
||||
.filter(|s| s.name.as_deref() == Some("LOAD"))
|
||||
.collect();
|
||||
assert!(!load_segments.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_elf_stripped_detection() {
|
||||
let data =
|
||||
load_fixture("hello_elf_stripped");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let data = load_fixture("hello_elf_stripped");
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
assert!(result.is_stripped);
|
||||
assert!(result.anomalies.iter().any(|a| {
|
||||
matches!(
|
||||
a,
|
||||
FormatAnomaly::StrippedBinary
|
||||
)
|
||||
}));
|
||||
assert!(
|
||||
result
|
||||
.anomalies
|
||||
.iter()
|
||||
.any(|a| { matches!(a, FormatAnomaly::StrippedBinary) })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_elf_info_populated() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
let elf_info = result.elf_info.unwrap();
|
||||
assert!(!elf_info.os_abi.is_empty());
|
||||
|
|
@ -160,18 +134,10 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_elf_section_hashes() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
formats::parse_format(&data).unwrap();
|
||||
let result = formats::parse_format(&data).unwrap();
|
||||
|
||||
let text = result
|
||||
.sections
|
||||
.iter()
|
||||
.find(|s| s.name == ".text")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!text.sha256.is_empty(),
|
||||
".text section should have a hash"
|
||||
);
|
||||
let text = result.sections.iter().find(|s| s.name == ".text").unwrap();
|
||||
assert!(!text.sha256.is_empty(), ".text section should have a hash");
|
||||
assert_eq!(text.sha256.len(), 64);
|
||||
}
|
||||
|
||||
|
|
@ -184,8 +150,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn format_pass_populates_context() {
|
||||
use crate::pass::AnalysisPass;
|
||||
use super::FormatPass;
|
||||
use crate::pass::AnalysisPass;
|
||||
|
||||
let data = load_fixture("hello_elf");
|
||||
let mut ctx = make_ctx(data);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ use crate::error::EngineError;
|
|||
use crate::pass::{AnalysisPass, Sealed};
|
||||
use crate::types::Severity;
|
||||
|
||||
type ImportsExportsLibs = (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>);
|
||||
|
||||
pub struct SuspiciousApiDef {
|
||||
pub name: &'static str,
|
||||
pub tag: &'static str,
|
||||
|
|
@ -179,11 +181,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
|
|||
name: "Process Injection Chain",
|
||||
description: "VirtualAllocEx + WriteProcessMemory \
|
||||
+ CreateRemoteThread",
|
||||
patterns: &[
|
||||
"VirtualAllocEx",
|
||||
"WriteProcessMemory",
|
||||
"CreateRemoteThread",
|
||||
],
|
||||
patterns: &["VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread"],
|
||||
mitre_id: "T1055",
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
|
|
@ -211,50 +209,35 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
|
|||
CombinationDef {
|
||||
name: "DLL Injection",
|
||||
description: "LoadLibrary + CreateRemoteThread",
|
||||
patterns: &[
|
||||
"LoadLibrary*",
|
||||
"CreateRemoteThread",
|
||||
],
|
||||
patterns: &["LoadLibrary*", "CreateRemoteThread"],
|
||||
mitre_id: "T1055.001",
|
||||
severity: Severity::High,
|
||||
},
|
||||
CombinationDef {
|
||||
name: "Credential Theft",
|
||||
description: "OpenProcess + ReadProcessMemory",
|
||||
patterns: &[
|
||||
"OpenProcess",
|
||||
"ReadProcessMemory",
|
||||
],
|
||||
patterns: &["OpenProcess", "ReadProcessMemory"],
|
||||
mitre_id: "T1003",
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
CombinationDef {
|
||||
name: "Service Persistence",
|
||||
description: "OpenSCManager + CreateService",
|
||||
patterns: &[
|
||||
"OpenSCManager*",
|
||||
"CreateService*",
|
||||
],
|
||||
patterns: &["OpenSCManager*", "CreateService*"],
|
||||
mitre_id: "T1543.003",
|
||||
severity: Severity::Medium,
|
||||
},
|
||||
CombinationDef {
|
||||
name: "Registry Persistence",
|
||||
description: "RegOpenKeyEx + RegSetValueEx",
|
||||
patterns: &[
|
||||
"RegOpenKeyEx*",
|
||||
"RegSetValueEx*",
|
||||
],
|
||||
patterns: &["RegOpenKeyEx*", "RegSetValueEx*"],
|
||||
mitre_id: "T1547.001",
|
||||
severity: Severity::Medium,
|
||||
},
|
||||
CombinationDef {
|
||||
name: "Download and Execute",
|
||||
description: "URLDownloadToFile + ShellExecute",
|
||||
patterns: &[
|
||||
"URLDownloadToFile*",
|
||||
"ShellExecute*",
|
||||
],
|
||||
patterns: &["URLDownloadToFile*", "ShellExecute*"],
|
||||
mitre_id: "T1105",
|
||||
severity: Severity::High,
|
||||
},
|
||||
|
|
@ -283,11 +266,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
|
|||
name: "Linux C2 Connection",
|
||||
description: "socket + connect + inet_pton \
|
||||
hardcoded C2 address",
|
||||
patterns: &[
|
||||
"socket",
|
||||
"connect",
|
||||
"inet_pton",
|
||||
],
|
||||
patterns: &["socket", "connect", "inet_pton"],
|
||||
mitre_id: "T1071",
|
||||
severity: Severity::High,
|
||||
},
|
||||
|
|
@ -295,12 +274,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
|
|||
name: "Linux Network Listener",
|
||||
description: "socket + bind + listen + accept \
|
||||
backdoor listener",
|
||||
patterns: &[
|
||||
"socket",
|
||||
"bind",
|
||||
"listen",
|
||||
"accept",
|
||||
],
|
||||
patterns: &["socket", "bind", "listen", "accept"],
|
||||
mitre_id: "T1571",
|
||||
severity: Severity::High,
|
||||
},
|
||||
|
|
@ -327,8 +301,7 @@ pub struct ImportResult {
|
|||
pub imports: Vec<ImportEntry>,
|
||||
pub exports: Vec<ExportEntry>,
|
||||
pub libraries: Vec<String>,
|
||||
pub suspicious_combinations:
|
||||
Vec<SuspiciousCombination>,
|
||||
pub suspicious_combinations: Vec<SuspiciousCombination>,
|
||||
pub mitre_mappings: Vec<MitreMapping>,
|
||||
pub statistics: ImportStatistics,
|
||||
}
|
||||
|
|
@ -389,43 +362,28 @@ impl AnalysisPass for ImportPass {
|
|||
&["format"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
|
||||
let result = analyze_imports(ctx.data())?;
|
||||
ctx.import_result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_imports(
|
||||
data: &[u8],
|
||||
) -> Result<ImportResult, EngineError> {
|
||||
let object =
|
||||
goblin::Object::parse(data).map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
fn analyze_imports(data: &[u8]) -> Result<ImportResult, EngineError> {
|
||||
let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let (imports, exports, libraries) = match &object {
|
||||
goblin::Object::Elf(elf) => extract_elf(elf),
|
||||
goblin::Object::PE(pe) => extract_pe(pe),
|
||||
goblin::Object::Mach(mach) => {
|
||||
extract_mach(mach, data)?
|
||||
}
|
||||
goblin::Object::Mach(mach) => extract_mach(mach, data)?,
|
||||
_ => (Vec::new(), Vec::new(), Vec::new()),
|
||||
};
|
||||
|
||||
let suspicious_combinations =
|
||||
detect_combinations(&imports);
|
||||
let mitre_mappings =
|
||||
collect_mitre_mappings(&imports);
|
||||
let suspicious_count = imports
|
||||
.iter()
|
||||
.filter(|i| i.is_suspicious)
|
||||
.count();
|
||||
let suspicious_combinations = detect_combinations(&imports);
|
||||
let mitre_mappings = collect_mitre_mappings(&imports);
|
||||
let suspicious_count = imports.iter().filter(|i| i.is_suspicious).count();
|
||||
|
||||
let statistics = ImportStatistics {
|
||||
total_imports: imports.len(),
|
||||
|
|
@ -444,29 +402,19 @@ fn analyze_imports(
|
|||
})
|
||||
}
|
||||
|
||||
fn extract_elf(
|
||||
elf: &goblin::elf::Elf,
|
||||
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
|
||||
let libraries: Vec<String> = elf
|
||||
.libraries
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
fn extract_elf(elf: &goblin::elf::Elf) -> ImportsExportsLibs {
|
||||
let libraries: Vec<String> = elf.libraries.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let mut imports = Vec::new();
|
||||
for sym in elf.dynsyms.iter() {
|
||||
if !sym.is_import() || sym.st_name == 0 {
|
||||
continue;
|
||||
}
|
||||
let name = elf
|
||||
.dynstrtab
|
||||
.get_at(sym.st_name)
|
||||
.unwrap_or("");
|
||||
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (is_suspicious, threat_tags) =
|
||||
flag_suspicious(name);
|
||||
let (is_suspicious, threat_tags) = flag_suspicious(name);
|
||||
imports.push(ImportEntry {
|
||||
library: String::new(),
|
||||
function: name.to_string(),
|
||||
|
|
@ -479,16 +427,10 @@ fn extract_elf(
|
|||
|
||||
let mut exports = Vec::new();
|
||||
for sym in elf.dynsyms.iter() {
|
||||
if sym.is_import()
|
||||
|| sym.st_value == 0
|
||||
|| sym.st_name == 0
|
||||
{
|
||||
if sym.is_import() || sym.st_value == 0 || sym.st_name == 0 {
|
||||
continue;
|
||||
}
|
||||
let name = elf
|
||||
.dynstrtab
|
||||
.get_at(sym.st_name)
|
||||
.unwrap_or("");
|
||||
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -504,17 +446,14 @@ fn extract_elf(
|
|||
(imports, exports, libraries)
|
||||
}
|
||||
|
||||
fn extract_pe(
|
||||
pe: &goblin::pe::PE,
|
||||
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
|
||||
fn extract_pe(pe: &goblin::pe::PE) -> ImportsExportsLibs {
|
||||
let mut lib_set = HashSet::new();
|
||||
let mut imports = Vec::new();
|
||||
|
||||
for import in &pe.imports {
|
||||
let dll = import.dll.to_string();
|
||||
lib_set.insert(dll.clone());
|
||||
let (is_suspicious, threat_tags) =
|
||||
flag_suspicious(&import.name);
|
||||
let (is_suspicious, threat_tags) = flag_suspicious(&import.name);
|
||||
imports.push(ImportEntry {
|
||||
library: dll,
|
||||
function: import.name.to_string(),
|
||||
|
|
@ -528,16 +467,11 @@ fn extract_pe(
|
|||
let mut exports = Vec::new();
|
||||
for export in &pe.exports {
|
||||
let is_forwarded = export.reexport.is_some();
|
||||
let forward_target =
|
||||
export.reexport.as_ref().map(|r| match r {
|
||||
goblin::pe::export::Reexport::DLLName {
|
||||
export: name,
|
||||
lib,
|
||||
} => format!("{lib}!{name}"),
|
||||
goblin::pe::export::Reexport::DLLOrdinal {
|
||||
ordinal,
|
||||
lib,
|
||||
} => format!("{lib}!#{ordinal}"),
|
||||
let forward_target = export.reexport.as_ref().map(|r| match r {
|
||||
goblin::pe::export::Reexport::DLLName { export: name, lib } => format!("{lib}!{name}"),
|
||||
goblin::pe::export::Reexport::DLLOrdinal { ordinal, lib } => {
|
||||
format!("{lib}!#{ordinal}")
|
||||
}
|
||||
});
|
||||
exports.push(ExportEntry {
|
||||
name: export.name.map(|s| s.to_string()),
|
||||
|
|
@ -548,34 +482,20 @@ fn extract_pe(
|
|||
});
|
||||
}
|
||||
|
||||
let libraries: Vec<String> =
|
||||
lib_set.into_iter().collect();
|
||||
let libraries: Vec<String> = lib_set.into_iter().collect();
|
||||
(imports, exports, libraries)
|
||||
}
|
||||
|
||||
fn extract_mach(
|
||||
mach: &goblin::mach::Mach,
|
||||
data: &[u8],
|
||||
) -> Result<
|
||||
(Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>),
|
||||
EngineError,
|
||||
> {
|
||||
fn extract_mach(mach: &goblin::mach::Mach, data: &[u8]) -> Result<ImportsExportsLibs, EngineError> {
|
||||
match mach {
|
||||
goblin::mach::Mach::Binary(macho) => {
|
||||
Ok(extract_single_macho(macho))
|
||||
}
|
||||
goblin::mach::Mach::Binary(macho) => Ok(extract_single_macho(macho)),
|
||||
goblin::mach::Mach::Fat(fat) => {
|
||||
for arch in fat.iter_arches() {
|
||||
let arch = arch.map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
if let Some(arch) = fat.iter_arches().next() {
|
||||
let arch = arch.map_err(|e| EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let macho = goblin::mach::MachO::parse(
|
||||
data,
|
||||
arch.offset as usize,
|
||||
)
|
||||
.map_err(|e| {
|
||||
let macho =
|
||||
goblin::mach::MachO::parse(data, arch.offset as usize).map_err(|e| {
|
||||
EngineError::InvalidBinary {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
|
|
@ -587,14 +507,11 @@ fn extract_mach(
|
|||
}
|
||||
}
|
||||
|
||||
fn extract_single_macho(
|
||||
macho: &goblin::mach::MachO,
|
||||
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
|
||||
fn extract_single_macho(macho: &goblin::mach::MachO) -> ImportsExportsLibs {
|
||||
let mut imports = Vec::new();
|
||||
if let Ok(macho_imports) = macho.imports() {
|
||||
for imp in &macho_imports {
|
||||
let (is_suspicious, threat_tags) =
|
||||
flag_suspicious(imp.name);
|
||||
let (is_suspicious, threat_tags) = flag_suspicious(imp.name);
|
||||
imports.push(ImportEntry {
|
||||
library: imp.dylib.to_string(),
|
||||
function: imp.name.to_string(),
|
||||
|
|
@ -629,9 +546,7 @@ fn extract_single_macho(
|
|||
(imports, exports, libraries)
|
||||
}
|
||||
|
||||
fn flag_suspicious(
|
||||
name: &str,
|
||||
) -> (bool, Vec<String>) {
|
||||
fn flag_suspicious(name: &str) -> (bool, Vec<String>) {
|
||||
let mut tags = Vec::new();
|
||||
for api in SUSPICIOUS_APIS {
|
||||
if matches_api(name, api.name) {
|
||||
|
|
@ -642,24 +557,17 @@ fn flag_suspicious(
|
|||
(is_suspicious, tags)
|
||||
}
|
||||
|
||||
fn matches_api(
|
||||
import_name: &str,
|
||||
api_name: &str,
|
||||
) -> bool {
|
||||
fn matches_api(import_name: &str, api_name: &str) -> bool {
|
||||
if import_name == api_name {
|
||||
return true;
|
||||
}
|
||||
if import_name.starts_with(api_name) {
|
||||
let suffix = &import_name[api_name.len()..];
|
||||
if let Some(suffix) = import_name.strip_prefix(api_name) {
|
||||
return suffix == "A" || suffix == "W";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn matches_pattern(
|
||||
import_name: &str,
|
||||
pattern: &str,
|
||||
) -> bool {
|
||||
fn matches_pattern(import_name: &str, pattern: &str) -> bool {
|
||||
if let Some(prefix) = pattern.strip_suffix('*') {
|
||||
import_name.starts_with(prefix)
|
||||
} else {
|
||||
|
|
@ -667,13 +575,8 @@ fn matches_pattern(
|
|||
}
|
||||
}
|
||||
|
||||
fn detect_combinations(
|
||||
imports: &[ImportEntry],
|
||||
) -> Vec<SuspiciousCombination> {
|
||||
let function_names: Vec<&str> = imports
|
||||
.iter()
|
||||
.map(|i| i.function.as_str())
|
||||
.collect();
|
||||
fn detect_combinations(imports: &[ImportEntry]) -> Vec<SuspiciousCombination> {
|
||||
let function_names: Vec<&str> = imports.iter().map(|i| i.function.as_str()).collect();
|
||||
let mut results = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
|
|
@ -681,11 +584,10 @@ fn detect_combinations(
|
|||
if seen.contains(combo.name) {
|
||||
continue;
|
||||
}
|
||||
let all_matched =
|
||||
combo.patterns.iter().all(|pattern| {
|
||||
function_names.iter().any(|name| {
|
||||
matches_pattern(name, pattern)
|
||||
})
|
||||
let all_matched = combo.patterns.iter().all(|pattern| {
|
||||
function_names
|
||||
.iter()
|
||||
.any(|name| matches_pattern(name, pattern))
|
||||
});
|
||||
if !all_matched {
|
||||
continue;
|
||||
|
|
@ -697,9 +599,7 @@ fn detect_combinations(
|
|||
.filter_map(|pattern| {
|
||||
function_names
|
||||
.iter()
|
||||
.find(|name| {
|
||||
matches_pattern(name, pattern)
|
||||
})
|
||||
.find(|name| matches_pattern(name, pattern))
|
||||
.map(|name| name.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -717,18 +617,14 @@ fn detect_combinations(
|
|||
results
|
||||
}
|
||||
|
||||
fn collect_mitre_mappings(
|
||||
imports: &[ImportEntry],
|
||||
) -> Vec<MitreMapping> {
|
||||
fn collect_mitre_mappings(imports: &[ImportEntry]) -> Vec<MitreMapping> {
|
||||
let mut mappings = Vec::new();
|
||||
for import in imports {
|
||||
if !import.is_suspicious {
|
||||
continue;
|
||||
}
|
||||
for api in SUSPICIOUS_APIS {
|
||||
if matches_api(&import.function, api.name)
|
||||
&& !api.mitre_id.is_empty()
|
||||
{
|
||||
if matches_api(&import.function, api.name) && !api.mitre_id.is_empty() {
|
||||
mappings.push(MitreMapping {
|
||||
technique_id: api.mitre_id.into(),
|
||||
api: import.function.clone(),
|
||||
|
|
@ -748,13 +644,8 @@ mod tests {
|
|||
use crate::context::BinarySource;
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
|
||||
|
|
@ -770,13 +661,9 @@ mod tests {
|
|||
#[test]
|
||||
fn elf_imports_extracted() {
|
||||
let data = load_fixture("hello_elf");
|
||||
let result =
|
||||
analyze_imports(&data).unwrap();
|
||||
let result = analyze_imports(&data).unwrap();
|
||||
|
||||
assert!(
|
||||
!result.imports.is_empty(),
|
||||
"ELF binary should have imports"
|
||||
);
|
||||
assert!(!result.imports.is_empty(), "ELF binary should have imports");
|
||||
assert!(
|
||||
!result.libraries.is_empty(),
|
||||
"ELF binary should list needed libraries"
|
||||
|
|
@ -786,20 +673,15 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn suspicious_api_flagging() {
|
||||
let (is_suspicious, tags) =
|
||||
flag_suspicious("VirtualAllocEx");
|
||||
let (is_suspicious, tags) = flag_suspicious("VirtualAllocEx");
|
||||
assert!(is_suspicious);
|
||||
assert!(tags.contains(&"injection".to_string()));
|
||||
|
||||
let (is_suspicious, tags) =
|
||||
flag_suspicious("RegSetValueExW");
|
||||
let (is_suspicious, tags) = flag_suspicious("RegSetValueExW");
|
||||
assert!(is_suspicious);
|
||||
assert!(tags.contains(
|
||||
&"persistence".to_string()
|
||||
));
|
||||
assert!(tags.contains(&"persistence".to_string()));
|
||||
|
||||
let (is_suspicious, _) =
|
||||
flag_suspicious("printf");
|
||||
let (is_suspicious, _) = flag_suspicious("printf");
|
||||
assert!(!is_suspicious);
|
||||
}
|
||||
|
||||
|
|
@ -812,9 +694,7 @@ mod tests {
|
|||
address: None,
|
||||
ordinal: None,
|
||||
is_suspicious: true,
|
||||
threat_tags: vec![
|
||||
"injection".into(),
|
||||
],
|
||||
threat_tags: vec!["injection".into()],
|
||||
},
|
||||
ImportEntry {
|
||||
library: "kernel32.dll".into(),
|
||||
|
|
@ -822,9 +702,7 @@ mod tests {
|
|||
address: None,
|
||||
ordinal: None,
|
||||
is_suspicious: true,
|
||||
threat_tags: vec![
|
||||
"injection".into(),
|
||||
],
|
||||
threat_tags: vec!["injection".into()],
|
||||
},
|
||||
ImportEntry {
|
||||
library: "kernel32.dll".into(),
|
||||
|
|
@ -832,23 +710,15 @@ mod tests {
|
|||
address: None,
|
||||
ordinal: None,
|
||||
is_suspicious: true,
|
||||
threat_tags: vec![
|
||||
"injection".into(),
|
||||
],
|
||||
threat_tags: vec!["injection".into()],
|
||||
},
|
||||
];
|
||||
|
||||
let combos = detect_combinations(&imports);
|
||||
assert_eq!(combos.len(), 1);
|
||||
assert_eq!(
|
||||
combos[0].name,
|
||||
"Process Injection Chain"
|
||||
);
|
||||
assert_eq!(combos[0].name, "Process Injection Chain");
|
||||
assert_eq!(combos[0].mitre_id, "T1055");
|
||||
assert_eq!(
|
||||
combos[0].severity,
|
||||
Severity::Critical
|
||||
);
|
||||
assert_eq!(combos[0].severity, Severity::Critical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -868,17 +738,12 @@ mod tests {
|
|||
address: None,
|
||||
ordinal: None,
|
||||
is_suspicious: true,
|
||||
threat_tags: vec![
|
||||
"persistence".into(),
|
||||
],
|
||||
threat_tags: vec!["persistence".into()],
|
||||
},
|
||||
];
|
||||
|
||||
let combos = detect_combinations(&imports);
|
||||
assert!(combos
|
||||
.iter()
|
||||
.any(|c| c.name
|
||||
== "Registry Persistence"));
|
||||
assert!(combos.iter().any(|c| c.name == "Registry Persistence"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -894,8 +759,7 @@ mod tests {
|
|||
|
||||
let combos = detect_combinations(&imports);
|
||||
assert!(
|
||||
!combos.iter().any(|c| c.name
|
||||
== "Process Injection Chain"),
|
||||
!combos.iter().any(|c| c.name == "Process Injection Chain"),
|
||||
"should not detect chain with only one API"
|
||||
);
|
||||
}
|
||||
|
|
@ -909,9 +773,7 @@ mod tests {
|
|||
address: None,
|
||||
ordinal: None,
|
||||
is_suspicious: true,
|
||||
threat_tags: vec![
|
||||
"injection".into(),
|
||||
],
|
||||
threat_tags: vec!["injection".into()],
|
||||
},
|
||||
ImportEntry {
|
||||
library: String::new(),
|
||||
|
|
@ -923,13 +785,9 @@ mod tests {
|
|||
},
|
||||
];
|
||||
|
||||
let mappings =
|
||||
collect_mitre_mappings(&imports);
|
||||
let mappings = collect_mitre_mappings(&imports);
|
||||
assert_eq!(mappings.len(), 1);
|
||||
assert_eq!(
|
||||
mappings[0].technique_id,
|
||||
"T1055.008"
|
||||
);
|
||||
assert_eq!(mappings[0].technique_id, "T1055.008");
|
||||
assert_eq!(mappings[0].api, "ptrace");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,23 +70,14 @@ const SUSPICIOUS_CATEGORIES: &[StringCategory] = &[
|
|||
StringCategory::CryptoWallet,
|
||||
];
|
||||
|
||||
const URL_PREFIXES: &[&str] =
|
||||
&["http://", "https://", "ftp://"];
|
||||
const URL_PREFIXES: &[&str] = &["http://", "https://", "ftp://"];
|
||||
|
||||
const UNIX_PATH_PREFIXES: &[&str] = &[
|
||||
"/etc/", "/tmp/", "/var/", "/bin/", "/usr/",
|
||||
"/dev/", "/proc/", "/sys/", "/opt/", "/home/",
|
||||
"/etc/", "/tmp/", "/var/", "/bin/", "/usr/", "/dev/", "/proc/", "/sys/", "/opt/", "/home/",
|
||||
"/root/", "/lib/", "/sbin/",
|
||||
];
|
||||
|
||||
const REGISTRY_PREFIXES: &[&str] = &[
|
||||
"HKEY_",
|
||||
"HKLM\\",
|
||||
"HKCU\\",
|
||||
"HKCR\\",
|
||||
"HKCC\\",
|
||||
"HKU\\",
|
||||
];
|
||||
const REGISTRY_PREFIXES: &[&str] = &["HKEY_", "HKLM\\", "HKCU\\", "HKCR\\", "HKCC\\", "HKU\\"];
|
||||
|
||||
const SHELL_INDICATORS: &[&str] = &[
|
||||
"cmd.exe",
|
||||
|
|
@ -108,8 +99,7 @@ const SHELL_INDICATORS: &[&str] = &[
|
|||
];
|
||||
|
||||
const PACKER_SIGNATURES: &[&str] = &[
|
||||
"UPX!", "MPRESS", ".themida", ".vmp", ".enigma",
|
||||
"PEC2", "ASPack", "MEW ",
|
||||
"UPX!", "MPRESS", ".themida", ".vmp", ".enigma", "PEC2", "ASPack", "MEW ",
|
||||
];
|
||||
|
||||
const DEBUG_ARTIFACTS: &[&str] = &[
|
||||
|
|
@ -163,8 +153,7 @@ const PERSISTENCE_PATHS: &[&str] = &[
|
|||
".config/autostart",
|
||||
];
|
||||
|
||||
const BASE64_CHARS: &[u8] =
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
const BASE64_MIN_LENGTH: usize = 20;
|
||||
|
||||
|
|
@ -215,25 +204,15 @@ impl AnalysisPass for StringPass {
|
|||
&["format"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &mut AnalysisContext,
|
||||
) -> Result<(), EngineError> {
|
||||
let sections = ctx
|
||||
.format_result
|
||||
.as_ref()
|
||||
.map(|fr| fr.sections.as_slice());
|
||||
let result =
|
||||
extract_strings(ctx.data(), sections);
|
||||
fn run(&self, ctx: &mut AnalysisContext) -> 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);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_strings(
|
||||
data: &[u8],
|
||||
sections: Option<&[SectionInfo]>,
|
||||
) -> StringResult {
|
||||
fn extract_strings(data: &[u8], sections: Option<&[SectionInfo]>) -> StringResult {
|
||||
let mut strings = Vec::new();
|
||||
|
||||
extract_ascii(data, sections, &mut strings);
|
||||
|
|
@ -244,12 +223,8 @@ fn extract_strings(
|
|||
let mut suspicious_count = 0;
|
||||
|
||||
for s in &strings {
|
||||
*by_encoding
|
||||
.entry(s.encoding.clone())
|
||||
.or_insert(0) += 1;
|
||||
*by_category
|
||||
.entry(s.category.clone())
|
||||
.or_insert(0) += 1;
|
||||
*by_encoding.entry(s.encoding.clone()).or_insert(0) += 1;
|
||||
*by_category.entry(s.category.clone()).or_insert(0) += 1;
|
||||
if s.is_suspicious {
|
||||
suspicious_count += 1;
|
||||
}
|
||||
|
|
@ -270,17 +245,10 @@ fn extract_strings(
|
|||
}
|
||||
|
||||
fn is_printable(b: u8) -> bool {
|
||||
(PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b)
|
||||
|| b == TAB
|
||||
|| b == NEWLINE
|
||||
|| b == CARRIAGE_RETURN
|
||||
(PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b) || b == TAB || b == NEWLINE || b == CARRIAGE_RETURN
|
||||
}
|
||||
|
||||
fn extract_ascii(
|
||||
data: &[u8],
|
||||
sections: Option<&[SectionInfo]>,
|
||||
out: &mut Vec<ExtractedString>,
|
||||
) {
|
||||
fn extract_ascii(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
|
||||
let mut start = 0;
|
||||
let mut in_run = false;
|
||||
|
||||
|
|
@ -292,27 +260,18 @@ fn extract_ascii(
|
|||
}
|
||||
} else if in_run {
|
||||
let len = i - start;
|
||||
if len >= MIN_STRING_LENGTH {
|
||||
if let Ok(s) =
|
||||
std::str::from_utf8(&data[start..i])
|
||||
if len >= MIN_STRING_LENGTH
|
||||
&& let Ok(s) = std::str::from_utf8(&data[start..i])
|
||||
{
|
||||
let is_multibyte = s
|
||||
.bytes()
|
||||
.any(|b| !b.is_ascii());
|
||||
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,
|
||||
)
|
||||
});
|
||||
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,
|
||||
|
|
@ -323,30 +282,24 @@ fn extract_ascii(
|
|||
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..])
|
||||
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 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)
|
||||
});
|
||||
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,
|
||||
|
|
@ -358,14 +311,9 @@ fn extract_ascii(
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_utf16le(
|
||||
data: &[u8],
|
||||
sections: Option<&[SectionInfo]>,
|
||||
out: &mut Vec<ExtractedString>,
|
||||
) {
|
||||
fn extract_utf16le(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
|
||||
let mut i = 0;
|
||||
while i + 1 < data.len() {
|
||||
let lo = data[i];
|
||||
|
|
@ -391,14 +339,10 @@ fn extract_utf16le(
|
|||
}
|
||||
|
||||
if code_units.len() >= MIN_STRING_LENGTH {
|
||||
let value =
|
||||
String::from_utf16_lossy(&code_units);
|
||||
let value = String::from_utf16_lossy(&code_units);
|
||||
let category = classify(&value);
|
||||
let is_suspicious =
|
||||
is_category_suspicious(&category);
|
||||
let section = sections.and_then(|secs| {
|
||||
find_section(secs, start as u64)
|
||||
});
|
||||
let is_suspicious = is_category_suspicious(&category);
|
||||
let section = sections.and_then(|secs| find_section(secs, start as u64));
|
||||
out.push(ExtractedString {
|
||||
value,
|
||||
offset: start as u64,
|
||||
|
|
@ -417,17 +361,11 @@ fn extract_utf16le(
|
|||
}
|
||||
}
|
||||
|
||||
fn find_section(
|
||||
sections: &[SectionInfo],
|
||||
file_offset: u64,
|
||||
) -> Option<String> {
|
||||
fn find_section(sections: &[SectionInfo], file_offset: u64) -> Option<String> {
|
||||
sections
|
||||
.iter()
|
||||
.find(|s| {
|
||||
s.raw_size > 0
|
||||
&& file_offset >= s.raw_offset
|
||||
&& file_offset
|
||||
< s.raw_offset + s.raw_size
|
||||
s.raw_size > 0 && file_offset >= s.raw_offset && file_offset < s.raw_offset + s.raw_size
|
||||
})
|
||||
.map(|s| s.name.clone())
|
||||
}
|
||||
|
|
@ -475,17 +413,13 @@ fn classify(s: &str) -> StringCategory {
|
|||
StringCategory::Generic
|
||||
}
|
||||
|
||||
fn is_category_suspicious(
|
||||
category: &StringCategory,
|
||||
) -> bool {
|
||||
fn is_category_suspicious(category: &StringCategory) -> bool {
|
||||
SUSPICIOUS_CATEGORIES.contains(category)
|
||||
}
|
||||
|
||||
fn is_url(s: &str) -> bool {
|
||||
let lower = s.to_ascii_lowercase();
|
||||
URL_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| lower.starts_with(prefix))
|
||||
URL_PREFIXES.iter().any(|prefix| lower.starts_with(prefix))
|
||||
}
|
||||
|
||||
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 {
|
||||
REGISTRY_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| s.starts_with(prefix))
|
||||
REGISTRY_PREFIXES.iter().any(|prefix| s.starts_with(prefix))
|
||||
}
|
||||
|
||||
fn is_shell_command(s: &str) -> bool {
|
||||
|
|
@ -537,11 +469,10 @@ fn is_crypto_wallet(s: &str) -> bool {
|
|||
if s.len() >= BTC_MIN_LENGTH
|
||||
&& s.len() <= BTC_MAX_LENGTH
|
||||
&& (s.starts_with('1') || s.starts_with('3'))
|
||||
&& s.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric())
|
||||
&& !s.chars().any(|c| {
|
||||
c == '0' || c == 'O' || c == 'I' || c == 'l'
|
||||
})
|
||||
&& s.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
&& !s
|
||||
.chars()
|
||||
.any(|c| c == '0' || c == 'O' || c == 'I' || c == 'l')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -572,51 +503,35 @@ fn is_email(s: &str) -> bool {
|
|||
return false;
|
||||
}
|
||||
let local = &s[..at_pos];
|
||||
local
|
||||
local.chars().all(|c| {
|
||||
c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '%' || c == '+' || c == '-'
|
||||
}) && domain[..dot_pos]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric()
|
||||
|| c == '.'
|
||||
|| c == '_'
|
||||
|| c == '%'
|
||||
|| c == '+'
|
||||
|| c == '-')
|
||||
&& domain[..dot_pos]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric()
|
||||
|| c == '.'
|
||||
|| c == '-')
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
|
||||
&& tld.chars().all(|c| c.is_ascii_alphabetic())
|
||||
}
|
||||
|
||||
fn is_suspicious_api(s: &str) -> bool {
|
||||
SUSPICIOUS_APIS
|
||||
.iter()
|
||||
.any(|api| s == api.name)
|
||||
SUSPICIOUS_APIS.iter().any(|api| s == api.name)
|
||||
}
|
||||
|
||||
fn is_packer_signature(s: &str) -> bool {
|
||||
PACKER_SIGNATURES
|
||||
.iter()
|
||||
.any(|sig| s.contains(sig))
|
||||
PACKER_SIGNATURES.iter().any(|sig| s.contains(sig))
|
||||
}
|
||||
|
||||
fn is_debug_artifact(s: &str) -> bool {
|
||||
DEBUG_ARTIFACTS
|
||||
.iter()
|
||||
.any(|artifact| s.contains(artifact))
|
||||
DEBUG_ARTIFACTS.iter().any(|artifact| s.contains(artifact))
|
||||
}
|
||||
|
||||
fn is_anti_analysis(s: &str) -> bool {
|
||||
let lower = s.to_ascii_lowercase();
|
||||
ANTI_ANALYSIS_INDICATORS.iter().any(|indicator| {
|
||||
lower.contains(&indicator.to_ascii_lowercase())
|
||||
})
|
||||
ANTI_ANALYSIS_INDICATORS
|
||||
.iter()
|
||||
.any(|indicator| lower.contains(&indicator.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
fn is_persistence_path(s: &str) -> bool {
|
||||
PERSISTENCE_PATHS
|
||||
.iter()
|
||||
.any(|path| s.contains(path))
|
||||
PERSISTENCE_PATHS.iter().any(|path| s.contains(path))
|
||||
}
|
||||
|
||||
fn is_encoded_data(s: &str) -> bool {
|
||||
|
|
@ -627,9 +542,7 @@ fn is_encoded_data(s: &str) -> bool {
|
|||
if trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let all_base64 = trimmed
|
||||
.bytes()
|
||||
.all(|b| BASE64_CHARS.contains(&b));
|
||||
let all_base64 = trimmed.bytes().all(|b| BASE64_CHARS.contains(&b));
|
||||
if !all_base64 {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -645,13 +558,8 @@ mod tests {
|
|||
use crate::context::BinarySource;
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
|
||||
|
|
@ -666,23 +574,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn ascii_extraction_min_length() {
|
||||
let data =
|
||||
b"abc\x00abcdef\x00ab\x00longstring\x00";
|
||||
let data = b"abc\x00abcdef\x00ab\x00longstring\x00";
|
||||
let result = extract_strings(data, None);
|
||||
|
||||
let values: Vec<&str> = result
|
||||
.strings
|
||||
.iter()
|
||||
.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"
|
||||
);
|
||||
let values: Vec<&str> = result.strings.iter().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!(
|
||||
values.contains(&"abcdef"),
|
||||
"6-char string should be included"
|
||||
|
|
@ -704,57 +601,28 @@ mod tests {
|
|||
data.push(0x00);
|
||||
|
||||
let result = extract_strings(&data, None);
|
||||
let utf16_strings: Vec<&ExtractedString> =
|
||||
result
|
||||
let utf16_strings: Vec<&ExtractedString> = result
|
||||
.strings
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
s.encoding == StringEncoding::Utf16Le
|
||||
})
|
||||
.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")));
|
||||
assert!(!utf16_strings.is_empty(), "should extract UTF-16LE string");
|
||||
assert!(utf16_strings.iter().any(|s| s.value.contains("Hello")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_url() {
|
||||
assert_eq!(
|
||||
classify("https://evil.com/payload"),
|
||||
StringCategory::Url,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("http://malware.ru/dropper"),
|
||||
StringCategory::Url,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("ftp://files.example.com"),
|
||||
StringCategory::Url,
|
||||
);
|
||||
assert_eq!(classify("https://evil.com/payload"), StringCategory::Url,);
|
||||
assert_eq!(classify("http://malware.ru/dropper"), StringCategory::Url,);
|
||||
assert_eq!(classify("ftp://files.example.com"), StringCategory::Url,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_ip_address() {
|
||||
assert_eq!(
|
||||
classify("192.168.1.1"),
|
||||
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,
|
||||
);
|
||||
assert_eq!(classify("192.168.1.1"), 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]
|
||||
|
|
@ -763,10 +631,7 @@ mod tests {
|
|||
classify("HKLM\\Software\\Microsoft"),
|
||||
StringCategory::RegistryKey,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("HKEY_LOCAL_MACHINE"),
|
||||
StringCategory::RegistryKey,
|
||||
);
|
||||
assert_eq!(classify("HKEY_LOCAL_MACHINE"), StringCategory::RegistryKey,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -775,22 +640,13 @@ mod tests {
|
|||
classify("C:\\Windows\\System32\\notepad.exe"),
|
||||
StringCategory::FilePath,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("/tmp/output.log"),
|
||||
StringCategory::FilePath,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("\\\\server\\share"),
|
||||
StringCategory::FilePath,
|
||||
);
|
||||
assert_eq!(classify("/tmp/output.log"), StringCategory::FilePath,);
|
||||
assert_eq!(classify("\\\\server\\share"), StringCategory::FilePath,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_shell_command() {
|
||||
assert_eq!(
|
||||
classify("cmd.exe /c whoami"),
|
||||
StringCategory::ShellCommand,
|
||||
);
|
||||
assert_eq!(classify("cmd.exe /c whoami"), StringCategory::ShellCommand,);
|
||||
assert_eq!(
|
||||
classify("/bin/bash -c echo hi"),
|
||||
StringCategory::ShellCommand,
|
||||
|
|
@ -799,10 +655,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn category_packer_signature() {
|
||||
assert_eq!(
|
||||
classify("UPX!"),
|
||||
StringCategory::PackerSignature,
|
||||
);
|
||||
assert_eq!(classify("UPX!"), StringCategory::PackerSignature,);
|
||||
assert_eq!(
|
||||
classify("This is .themida packed"),
|
||||
StringCategory::PackerSignature,
|
||||
|
|
@ -815,10 +668,7 @@ mod tests {
|
|||
classify("VMware Virtual Platform"),
|
||||
StringCategory::AntiAnalysis,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("wireshark.exe"),
|
||||
StringCategory::AntiAnalysis,
|
||||
);
|
||||
assert_eq!(classify("wireshark.exe"), StringCategory::AntiAnalysis,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -830,63 +680,34 @@ mod tests {
|
|||
),
|
||||
StringCategory::PersistencePath,
|
||||
);
|
||||
assert_eq!(
|
||||
classify("/etc/crontab"),
|
||||
StringCategory::PersistencePath,
|
||||
);
|
||||
assert_eq!(classify("/etc/crontab"), StringCategory::PersistencePath,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_encoded_data() {
|
||||
assert_eq!(
|
||||
classify(
|
||||
"SGVsbG8gV29ybGQhIFRoaXMgaXM="
|
||||
),
|
||||
StringCategory::EncodedData,
|
||||
);
|
||||
assert_ne!(
|
||||
classify("short"),
|
||||
classify("SGVsbG8gV29ybGQhIFRoaXMgaXM="),
|
||||
StringCategory::EncodedData,
|
||||
);
|
||||
assert_ne!(classify("short"), StringCategory::EncodedData,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_email() {
|
||||
assert_eq!(
|
||||
classify("user@example.com"),
|
||||
StringCategory::Email,
|
||||
);
|
||||
assert_eq!(classify("user@example.com"), StringCategory::Email,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspicious_flag_by_category() {
|
||||
assert!(is_category_suspicious(
|
||||
&StringCategory::ShellCommand
|
||||
));
|
||||
assert!(is_category_suspicious(
|
||||
&StringCategory::AntiAnalysis
|
||||
));
|
||||
assert!(is_category_suspicious(
|
||||
&StringCategory::PackerSignature
|
||||
));
|
||||
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
|
||||
));
|
||||
assert!(is_category_suspicious(&StringCategory::ShellCommand));
|
||||
assert!(is_category_suspicious(&StringCategory::AntiAnalysis));
|
||||
assert!(is_category_suspicious(&StringCategory::PackerSignature));
|
||||
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]
|
||||
|
|
@ -919,8 +740,7 @@ mod tests {
|
|||
virtual_size: 100,
|
||||
raw_offset: 10,
|
||||
raw_size: 100,
|
||||
permissions:
|
||||
crate::types::SectionPermissions {
|
||||
permissions: crate::types::SectionPermissions {
|
||||
read: true,
|
||||
write: false,
|
||||
execute: false,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -271,57 +271,37 @@ pub struct YaraScanner {
|
|||
impl YaraScanner {
|
||||
pub fn new() -> Result<Self, EngineError> {
|
||||
let mut compiler = yara_x::Compiler::new();
|
||||
compiler.add_source(BUILTIN_RULES).map_err(
|
||||
|e| EngineError::Yara(e.to_string()),
|
||||
)?;
|
||||
compiler
|
||||
.add_source(BUILTIN_RULES)
|
||||
.map_err(|e| EngineError::Yara(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
rules: compiler.build(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_custom_rules(
|
||||
rules_dir: &Path,
|
||||
) -> Result<Self, EngineError> {
|
||||
pub fn with_custom_rules(rules_dir: &Path) -> Result<Self, EngineError> {
|
||||
let mut compiler = yara_x::Compiler::new();
|
||||
compiler.add_source(BUILTIN_RULES).map_err(
|
||||
|e| EngineError::Yara(e.to_string()),
|
||||
)?;
|
||||
compiler
|
||||
.add_source(BUILTIN_RULES)
|
||||
.map_err(|e| EngineError::Yara(e.to_string()))?;
|
||||
|
||||
if rules_dir.is_dir() {
|
||||
for entry in std::fs::read_dir(rules_dir)
|
||||
.map_err(|e| {
|
||||
EngineError::Yara(format!(
|
||||
"failed to read rules dir: {e}"
|
||||
))
|
||||
})?
|
||||
.map_err(|e| EngineError::Yara(format!("failed to read rules dir: {e}")))?
|
||||
{
|
||||
let entry = entry.map_err(|e| {
|
||||
EngineError::Yara(format!(
|
||||
"dir entry error: {e}"
|
||||
))
|
||||
})?;
|
||||
let entry =
|
||||
entry.map_err(|e| EngineError::Yara(format!("dir entry error: {e}")))?;
|
||||
let path = entry.path();
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext == "yar" || ext == "yara")
|
||||
{
|
||||
let source = std::fs::read_to_string(
|
||||
&path,
|
||||
)
|
||||
.map_err(|e| {
|
||||
EngineError::Yara(format!(
|
||||
"failed to read {}: {e}",
|
||||
path.display()
|
||||
))
|
||||
let source = std::fs::read_to_string(&path).map_err(|e| {
|
||||
EngineError::Yara(format!("failed to read {}: {e}", path.display()))
|
||||
})?;
|
||||
compiler
|
||||
.add_source(source.as_str())
|
||||
.map_err(|e| {
|
||||
EngineError::Yara(format!(
|
||||
"compile error in {}: {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(
|
||||
&self,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<YaraMatch>, EngineError> {
|
||||
let mut scanner =
|
||||
yara_x::Scanner::new(&self.rules);
|
||||
let results = scanner.scan(data).map_err(
|
||||
|e| EngineError::Yara(e.to_string()),
|
||||
)?;
|
||||
pub fn scan(&self, data: &[u8]) -> Result<Vec<YaraMatch>, EngineError> {
|
||||
let mut scanner = yara_x::Scanner::new(&self.rules);
|
||||
let results = scanner
|
||||
.scan(data)
|
||||
.map_err(|e| EngineError::Yara(e.to_string()))?;
|
||||
|
||||
let mut matches = Vec::new();
|
||||
for rule in results.matching_rules() {
|
||||
let tags: Vec<String> = rule
|
||||
.tags()
|
||||
.map(|t| t.identifier().to_string())
|
||||
.collect();
|
||||
let tags: Vec<String> = rule.tags().map(|t| t.identifier().to_string()).collect();
|
||||
|
||||
let mut description = None;
|
||||
let mut category = None;
|
||||
|
|
@ -356,20 +329,17 @@ impl YaraScanner {
|
|||
match key {
|
||||
"description" => {
|
||||
if let yara_x::MetaValue::String(s) = value {
|
||||
description =
|
||||
Some(s.to_string());
|
||||
description = Some(s.to_string());
|
||||
}
|
||||
}
|
||||
"category" => {
|
||||
if let yara_x::MetaValue::String(s) = value {
|
||||
category =
|
||||
Some(s.to_string());
|
||||
category = Some(s.to_string());
|
||||
}
|
||||
}
|
||||
"severity" => {
|
||||
if let yara_x::MetaValue::String(s) = value {
|
||||
severity =
|
||||
Some(s.to_string());
|
||||
severity = Some(s.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -378,25 +348,18 @@ impl YaraScanner {
|
|||
|
||||
let mut matched_strings = Vec::new();
|
||||
for pattern in rule.patterns() {
|
||||
let id = pattern
|
||||
.identifier()
|
||||
.to_string();
|
||||
let count =
|
||||
pattern.matches().count();
|
||||
let id = pattern.identifier().to_string();
|
||||
let count = pattern.matches().count();
|
||||
if count > 0 {
|
||||
matched_strings.push(
|
||||
YaraStringMatch {
|
||||
matched_strings.push(YaraStringMatch {
|
||||
identifier: id,
|
||||
match_count: count,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
matches.push(YaraMatch {
|
||||
rule_name: rule
|
||||
.identifier()
|
||||
.to_string(),
|
||||
rule_name: rule.identifier().to_string(),
|
||||
tags,
|
||||
metadata: YaraMetadata {
|
||||
description,
|
||||
|
|
@ -435,42 +398,25 @@ mod tests {
|
|||
let upx_match = result
|
||||
.iter()
|
||||
.find(|m| m.rule_name == "suspicious_upx_packed");
|
||||
assert!(
|
||||
upx_match.is_some(),
|
||||
"should detect UPX packer signature"
|
||||
);
|
||||
let meta =
|
||||
&upx_match.unwrap().metadata;
|
||||
assert_eq!(
|
||||
meta.category.as_deref(),
|
||||
Some("packer")
|
||||
);
|
||||
assert!(upx_match.is_some(), "should detect UPX packer signature");
|
||||
let meta = &upx_match.unwrap().metadata;
|
||||
assert_eq!(meta.category.as_deref(), Some("packer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_process_injection() {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(
|
||||
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\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(&[0u8; 256]);
|
||||
|
||||
let scanner = YaraScanner::new().unwrap();
|
||||
let result = scanner.scan(&data).unwrap();
|
||||
let injection = result.iter().find(|m| {
|
||||
m.rule_name
|
||||
== "suspicious_process_injection"
|
||||
});
|
||||
assert!(
|
||||
injection.is_some(),
|
||||
"should detect process injection APIs"
|
||||
);
|
||||
let injection = result
|
||||
.iter()
|
||||
.find(|m| m.rule_name == "suspicious_process_injection");
|
||||
assert!(injection.is_some(), "should detect process injection APIs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -480,9 +426,7 @@ mod tests {
|
|||
let result = scanner.scan(data).unwrap();
|
||||
let suspicious: Vec<_> = result
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.rule_name != "suspicious_obfuscation"
|
||||
})
|
||||
.filter(|m| m.rule_name != "suspicious_obfuscation")
|
||||
.collect();
|
||||
assert!(
|
||||
suspicious.is_empty(),
|
||||
|
|
@ -492,13 +436,8 @@ mod tests {
|
|||
}
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path).unwrap_or_else(|e| {
|
||||
panic!("fixture {path}: {e}")
|
||||
})
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -20,24 +20,19 @@
|
|||
// lib.rs - AnalysisEngine
|
||||
// types.rs - BinaryFormat
|
||||
|
||||
use axumortem_engine::types::BinaryFormat;
|
||||
use axumortem_engine::AnalysisEngine;
|
||||
use axumortem_engine::types::BinaryFormat;
|
||||
|
||||
fn load_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
);
|
||||
std::fs::read(&path)
|
||||
.unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
|
||||
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_pipeline_elf() {
|
||||
let engine = AnalysisEngine::new().unwrap();
|
||||
let data = load_fixture("hello_elf");
|
||||
let (ctx, report) =
|
||||
engine.analyze(&data, "hello_elf");
|
||||
let (ctx, report) = engine.analyze(&data, "hello_elf");
|
||||
|
||||
assert!(
|
||||
report.all_succeeded(),
|
||||
|
|
@ -58,8 +53,7 @@ fn full_pipeline_elf() {
|
|||
assert!(ctx.entropy_result.is_some());
|
||||
assert!(ctx.disassembly_result.is_some());
|
||||
|
||||
let disasm =
|
||||
ctx.disassembly_result.as_ref().unwrap();
|
||||
let disasm = ctx.disassembly_result.as_ref().unwrap();
|
||||
assert!(disasm.total_functions > 0);
|
||||
assert!(disasm.total_instructions > 0);
|
||||
|
||||
|
|
@ -73,8 +67,7 @@ fn full_pipeline_elf() {
|
|||
fn full_pipeline_stripped_elf() {
|
||||
let engine = AnalysisEngine::new().unwrap();
|
||||
let data = load_fixture("hello_elf_stripped");
|
||||
let (ctx, report) =
|
||||
engine.analyze(&data, "hello_elf_stripped");
|
||||
let (ctx, report) = engine.analyze(&data, "hello_elf_stripped");
|
||||
|
||||
assert!(report.all_succeeded());
|
||||
|
||||
|
|
@ -98,15 +91,11 @@ fn sha256_computed_correctly() {
|
|||
fn invalid_binary_handled() {
|
||||
let engine = AnalysisEngine::new().unwrap();
|
||||
let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
|
||||
let (_, report) =
|
||||
engine.analyze(&data, "garbage.bin");
|
||||
let (_, report) = engine.analyze(&data, "garbage.bin");
|
||||
|
||||
assert!(
|
||||
!report.all_succeeded(),
|
||||
"invalid binary should cause format pass failure"
|
||||
);
|
||||
assert!(report
|
||||
.failed_passes()
|
||||
.iter()
|
||||
.any(|p| p.name == "format"));
|
||||
assert!(report.failed_passes().iter().any(|p| p.name == "format"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ pub mod queries;
|
|||
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub async fn run_migrations(
|
||||
pool: &PgPool,
|
||||
) -> Result<(), sqlx::migrate::MigrateError> {
|
||||
pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
|
||||
sqlx::migrate!("./migrations").run(pool).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub struct AnalysisRow {
|
|||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PassResultRow {
|
||||
pub id: Uuid,
|
||||
pub analysis_id: Uuid,
|
||||
|
|
|
|||
|
|
@ -22,30 +22,20 @@
|
|||
use sqlx::{PgPool, Postgres, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::models::{
|
||||
AnalysisRow, NewAnalysis, NewPassResult,
|
||||
PassResultRow,
|
||||
};
|
||||
use super::models::{AnalysisRow, NewAnalysis, NewPassResult, PassResultRow};
|
||||
|
||||
pub async fn find_slug_by_sha256(
|
||||
pool: &PgPool,
|
||||
sha256: &str,
|
||||
) -> Result<Option<String>, sqlx::Error> {
|
||||
sqlx::query_scalar(
|
||||
"SELECT slug FROM analyses WHERE sha256 = $1",
|
||||
)
|
||||
sqlx::query_scalar("SELECT slug FROM analyses WHERE sha256 = $1")
|
||||
.bind(sha256)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_slug(
|
||||
pool: &PgPool,
|
||||
slug: &str,
|
||||
) -> Result<Option<AnalysisRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AnalysisRow>(
|
||||
"SELECT * FROM analyses WHERE slug = $1",
|
||||
)
|
||||
pub async fn find_by_slug(pool: &PgPool, slug: &str) -> Result<Option<AnalysisRow>, sqlx::Error> {
|
||||
sqlx::query_as::<_, AnalysisRow>("SELECT * FROM analyses WHERE slug = $1")
|
||||
.bind(slug)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
// routes/upload.rs - returned from upload handler
|
||||
// routes/analysis.rs - returned from analysis lookup
|
||||
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -32,6 +32,7 @@ struct ErrorDetail {
|
|||
message: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum ApiError {
|
||||
NoFile,
|
||||
FileTooLarge { max_bytes: usize },
|
||||
|
|
@ -71,37 +72,23 @@ impl IntoResponse for ApiError {
|
|||
Self::NoFile => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"NO_FILE",
|
||||
"No file was provided in the upload"
|
||||
.to_string(),
|
||||
"No file was provided in the upload".to_string(),
|
||||
),
|
||||
Self::FileTooLarge { max_bytes } => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"FILE_TOO_LARGE",
|
||||
format!(
|
||||
"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,
|
||||
format!("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::NotFound { resource } => (
|
||||
StatusCode::NOT_FOUND,
|
||||
"NOT_FOUND",
|
||||
format!("{resource} not found"),
|
||||
),
|
||||
Self::Internal { reason } => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"INTERNAL",
|
||||
reason,
|
||||
),
|
||||
Self::Internal { reason } => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL", reason),
|
||||
};
|
||||
|
||||
(
|
||||
|
|
|
|||
|
|
@ -49,11 +49,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"info,tower_http=debug",
|
||||
)
|
||||
}),
|
||||
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")),
|
||||
)
|
||||
.init();
|
||||
|
||||
|
|
@ -67,10 +63,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
.await
|
||||
.context("failed to run database migrations")?;
|
||||
|
||||
let engine = axumortem_engine::AnalysisEngine::new()
|
||||
.context(
|
||||
"failed to initialize analysis engine",
|
||||
)?;
|
||||
let engine =
|
||||
axumortem_engine::AnalysisEngine::new().context("failed to initialize analysis engine")?;
|
||||
|
||||
let config = Arc::new(config);
|
||||
|
||||
|
|
@ -83,16 +77,12 @@ async fn main() -> anyhow::Result<()> {
|
|||
let layers = ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(middleware::cors::layer(&config))
|
||||
.layer(DefaultBodyLimit::max(
|
||||
config.max_upload_size,
|
||||
));
|
||||
.layer(DefaultBodyLimit::max(config.max_upload_size));
|
||||
|
||||
let app =
|
||||
routes::api_router().layer(layers).with_state(state);
|
||||
let app = routes::api_router().layer(layers).with_state(state);
|
||||
|
||||
let bind_address = config.bind_address();
|
||||
let listener =
|
||||
tokio::net::TcpListener::bind(&bind_address)
|
||||
let listener = tokio::net::TcpListener::bind(&bind_address)
|
||||
.await
|
||||
.context("failed to bind TCP listener")?;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,17 +13,15 @@
|
|||
// config.rs - AppConfig.cors_origin
|
||||
// main.rs - applied as tower layer
|
||||
|
||||
use axum::http::header::{HeaderName, ACCEPT, CONTENT_TYPE};
|
||||
use axum::http::Method;
|
||||
use axum::http::header::{ACCEPT, CONTENT_TYPE, HeaderName};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
const ALLOWED_METHODS: [Method; 3] =
|
||||
[Method::GET, Method::POST, Method::OPTIONS];
|
||||
const ALLOWED_METHODS: [Method; 3] = [Method::GET, Method::POST, Method::OPTIONS];
|
||||
|
||||
const ALLOWED_HEADERS: [HeaderName; 2] =
|
||||
[CONTENT_TYPE, ACCEPT];
|
||||
const ALLOWED_HEADERS: [HeaderName; 2] = [CONTENT_TYPE, ACCEPT];
|
||||
|
||||
pub fn layer(config: &AppConfig) -> CorsLayer {
|
||||
let base = CorsLayer::new()
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -53,12 +53,9 @@ pub async fn get_by_slug(
|
|||
resource: format!("analysis '{slug}'"),
|
||||
})?;
|
||||
|
||||
let pass_rows =
|
||||
queries::find_pass_results(&state.db, row.id)
|
||||
.await?;
|
||||
let pass_rows = queries::find_pass_results(&state.db, row.id).await?;
|
||||
|
||||
let passes: HashMap<String, serde_json::Value> =
|
||||
pass_rows
|
||||
let passes: HashMap<String, serde_json::Value> = pass_rows
|
||||
.into_iter()
|
||||
.map(|p| (p.pass_name, p.result))
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
// Connects to:
|
||||
// state.rs - AppState.db
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
|
@ -23,12 +23,8 @@ pub(crate) struct HealthResponse {
|
|||
database: &'static str,
|
||||
}
|
||||
|
||||
pub async fn check(
|
||||
State(state): State<AppState>,
|
||||
) -> Json<HealthResponse> {
|
||||
let db_status =
|
||||
match sqlx::query("SELECT 1").execute(&state.db).await
|
||||
{
|
||||
pub async fn check(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||
let db_status = match sqlx::query("SELECT 1").execute(&state.db).await {
|
||||
Ok(_) => "connected",
|
||||
Err(_) => "disconnected",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ mod analysis;
|
|||
mod health;
|
||||
mod upload;
|
||||
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use axum::routing::{get, post};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
|
|
@ -27,8 +27,5 @@ pub fn api_router() -> Router<AppState> {
|
|||
Router::new()
|
||||
.route("/api/health", get(health::check))
|
||||
.route("/api/upload", post(upload::handle))
|
||||
.route(
|
||||
"/api/analysis/{slug}",
|
||||
get(analysis::get_by_slug),
|
||||
)
|
||||
.route("/api/analysis/{slug}", get(analysis::get_by_slug))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::Json;
|
||||
use axum::extract::{Multipart, State};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -42,9 +42,7 @@ use crate::state::AppState;
|
|||
|
||||
const SLUG_LENGTH: usize = 12;
|
||||
|
||||
const PASS_NAME_MAP: &[(&str, &str)] = &[
|
||||
("disasm", "disassembly"),
|
||||
];
|
||||
const PASS_NAME_MAP: &[(&str, &str)] = &[("disasm", "disassembly")];
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct UploadResponse {
|
||||
|
|
@ -56,32 +54,19 @@ pub async fn handle(
|
|||
State(state): State<AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<UploadResponse>, ApiError> {
|
||||
let (file_name, data) =
|
||||
extract_file(&mut multipart).await?;
|
||||
let (file_name, data) = extract_file(&mut multipart).await?;
|
||||
|
||||
let sha256 =
|
||||
axumortem_engine::sha256_hex(&data);
|
||||
let sha256 = axumortem_engine::sha256_hex(&data);
|
||||
|
||||
if let Some(slug) =
|
||||
queries::find_slug_by_sha256(
|
||||
&state.db, &sha256,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Json(UploadResponse {
|
||||
slug,
|
||||
cached: true,
|
||||
}));
|
||||
if let Some(slug) = queries::find_slug_by_sha256(&state.db, &sha256).await? {
|
||||
return Ok(Json(UploadResponse { slug, cached: true }));
|
||||
}
|
||||
|
||||
let engine = Arc::clone(&state.engine);
|
||||
let name_clone = file_name.clone();
|
||||
|
||||
let (ctx, report) =
|
||||
tokio::task::spawn_blocking(move || {
|
||||
engine.analyze(&data, &name_clone)
|
||||
})
|
||||
.await?;
|
||||
tokio::task::spawn_blocking(move || engine.analyze(&data, &name_clone)).await?;
|
||||
|
||||
let fmt = ctx.format_result.as_ref();
|
||||
let threat = ctx.threat_result.as_ref();
|
||||
|
|
@ -91,29 +76,19 @@ pub async fn handle(
|
|||
sha256,
|
||||
file_name,
|
||||
file_size: ctx.file_size as i64,
|
||||
format: fmt
|
||||
.map(|f| f.format.to_string())
|
||||
.unwrap_or_default(),
|
||||
architecture: fmt
|
||||
.map(|f| f.architecture.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()),
|
||||
format: fmt.map(|f| f.format.to_string()).unwrap_or_default(),
|
||||
architecture: fmt.map(|f| f.architecture.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(),
|
||||
};
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
let row =
|
||||
queries::insert_analysis(&mut tx, &new_analysis)
|
||||
.await?;
|
||||
let row = queries::insert_analysis(&mut tx, &new_analysis).await?;
|
||||
|
||||
let pass_results =
|
||||
build_pass_results(&ctx, &report, row.id)?;
|
||||
let pass_results = build_pass_results(&ctx, &report, row.id)?;
|
||||
for pr in &pass_results {
|
||||
queries::insert_pass_result(&mut tx, pr).await?;
|
||||
}
|
||||
|
|
@ -126,9 +101,7 @@ pub async fn handle(
|
|||
}))
|
||||
}
|
||||
|
||||
async fn extract_file(
|
||||
multipart: &mut Multipart,
|
||||
) -> Result<(String, Vec<u8>), ApiError> {
|
||||
async fn extract_file(multipart: &mut Multipart) -> Result<(String, Vec<u8>), ApiError> {
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
|
|
@ -137,14 +110,8 @@ async fn extract_file(
|
|||
})?
|
||||
{
|
||||
if field.name() == Some("file") {
|
||||
let name = field
|
||||
.file_name()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let data = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal {
|
||||
let name = field.file_name().unwrap_or("unknown").to_string();
|
||||
let data = field.bytes().await.map_err(|e| ApiError::Internal {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
return Ok((name, data.to_vec()));
|
||||
|
|
@ -181,12 +148,9 @@ fn build_pass_results(
|
|||
if let Some(ref r) = ctx.$field {
|
||||
results.push(NewPassResult {
|
||||
analysis_id,
|
||||
pass_name: api_name($name)
|
||||
.to_string(),
|
||||
pass_name: api_name($name).to_string(),
|
||||
result: serde_json::to_value(r)?,
|
||||
duration_ms: durations
|
||||
.get($name)
|
||||
.map(|&d| d as i32),
|
||||
duration_ms: durations.get($name).map(|&d| d as i32),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ use crate::config::AppConfig;
|
|||
pub struct AppState {
|
||||
pub db: PgPool,
|
||||
pub engine: Arc<AnalysisEngine>,
|
||||
#[allow(dead_code)]
|
||||
pub config: Arc<AppConfig>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export function Component(): React.ReactElement {
|
|||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<filter id="grain-bat">
|
||||
<feTurbulence
|
||||
|
|
@ -248,7 +249,9 @@ export function Component(): React.ReactElement {
|
|||
|
||||
<footer className={styles.footer}>
|
||||
<span>© 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>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
version: 2.0
|
||||
shards:
|
||||
ameba:
|
||||
git: https://github.com/crystal-ameba/ameba.git
|
||||
version: 1.6.4
|
||||
|
||||
db:
|
||||
git: https://github.com/crystal-lang/crystal-db.git
|
||||
version: 0.11.0
|
||||
|
|
|
|||
|
|
@ -33,3 +33,6 @@ development_dependencies:
|
|||
webmock:
|
||||
github: manastech/webmock.cr
|
||||
version: ~> 0.9
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.6.4
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ module CRE::Aws
|
|||
end
|
||||
|
||||
private def canonical_query(query : String?) : String
|
||||
return "" unless query && !query.empty?
|
||||
return "" if query.nil? || query.empty?
|
||||
params = [] of {String, String}
|
||||
query.split('&') do |pair|
|
||||
eq = pair.index('=')
|
||||
|
|
@ -99,7 +99,7 @@ module CRE::Aws
|
|||
private def canonical_headers_and_list(headers : HTTP::Headers) : {String, String}
|
||||
sorted = headers.to_a.map { |name, values|
|
||||
{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
|
||||
list = sorted.map(&.[0]).join(';')
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ module CRE::Cli::Commands
|
|||
CRE::Policy::Evaluator.new(bus, persist).evaluate_all
|
||||
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
|
||||
persist.close
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module CRE::Demo
|
|||
bus.run
|
||||
tui.start
|
||||
|
||||
script_fiber = spawn do
|
||||
spawn do
|
||||
run_script(bus, seconds)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ module CRE::Engine
|
|||
@persistence.rotations.update_state(rotation_id, Persistence::RotationState::Applying)
|
||||
@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)
|
||||
rotator.apply(c, new_secret)
|
||||
@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)
|
||||
begin
|
||||
rotator.rollback_apply(c, ns)
|
||||
rescue rb
|
||||
Log.error(exception: rb) { "rollback_apply failed for credential #{c.id}" }
|
||||
rescue error
|
||||
Log.error(exception: error) { "rollback_apply failed for credential #{c.id}" }
|
||||
end
|
||||
end
|
||||
finalize_failure(c, rotation_id, current_step, ex)
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ module CRE::Persistence::Postgres
|
|||
@audit ||= AuditRepo.new(@db)
|
||||
end
|
||||
|
||||
def transaction(&block : ->) : Nil
|
||||
def transaction(& : ->) : Nil
|
||||
@db.transaction { yield }
|
||||
end
|
||||
|
||||
def with_advisory_lock(key : Int64, &block : ->) : Nil
|
||||
def with_advisory_lock(key : Int64, & : ->) : Nil
|
||||
@db.transaction do |tx|
|
||||
tx.connection.exec("SELECT pg_advisory_xact_lock($1)", key)
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ module CRE::Persistence::Sqlite
|
|||
@audit ||= AuditRepo.new(@db)
|
||||
end
|
||||
|
||||
def transaction(&block : ->) : Nil
|
||||
def transaction(& : ->) : Nil
|
||||
@db.transaction { yield }
|
||||
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
|
||||
@mutex.synchronize { yield }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module CRE::Policy::Dsl
|
|||
# `enforce :rotate_immediatly` fail at compile time. Splat-symbol args
|
||||
# (`notify_via :telegram, :structured_log`) are validated at policy
|
||||
# registration time and raise `BuilderError` on typos.
|
||||
def policy(name : String, &block)
|
||||
def policy(name : String, &)
|
||||
builder = CRE::Policy::Builder.new(name)
|
||||
with builder yield
|
||||
CRE::Policy::REGISTRY << builder.build
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ module CRE::Policy
|
|||
return if @policies.empty?
|
||||
|
||||
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|
|
||||
next if seen.includes?(c.id)
|
||||
seen << c.id
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ module CRE::Rotators
|
|||
pending = pending_path(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)
|
||||
lines << "#{key}=#{new_value}"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,39 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .golangci.yml
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- staticcheck
|
||||
- unused
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- typecheck
|
||||
- gofmt
|
||||
- gocritic
|
||||
- gosec
|
||||
- misspell
|
||||
- unconvert
|
||||
- unparam
|
||||
- prealloc
|
||||
|
||||
linters-settings:
|
||||
settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- fmt.Fprint
|
||||
- fmt.Fprintf
|
||||
- fmt.Fprintln
|
||||
- (io.Closer).Close
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- performance
|
||||
disabled-checks:
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
gosec:
|
||||
excludes:
|
||||
- G304
|
||||
- G704
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func runGenerate(_ *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
if outputFile != "" {
|
||||
if err := os.WriteFile(outputFile, data, 0o644); err != nil {
|
||||
if err := os.WriteFile(outputFile, data, 0o600); err != nil {
|
||||
return fmt.Errorf("write output: %w", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " %s SBOM written to %s (%s)\n",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func runVuln(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnReport, error) {
|
||||
var allPkgs []types.Package
|
||||
allPkgs := make([]types.Package, 0, len(result.Graphs))
|
||||
for _, g := range result.Graphs {
|
||||
allPkgs = append(allPkgs, graph.AllPackages(g)...)
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnRepor
|
|||
c, err := vuln.NewCache(vuln.DefaultCachePath(), 24*time.Hour)
|
||||
if err == nil {
|
||||
cache = c
|
||||
defer cache.Close()
|
||||
defer func() { _ = cache.Close() }()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue