dsad
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
27
.mcp.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ssh-mcp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"ssh-mcp",
|
||||
"-y",
|
||||
"--",
|
||||
"--host=52.221.242.70",
|
||||
"--port=22",
|
||||
"--user=root",
|
||||
"--password=pass",
|
||||
"--key=path/to/key",
|
||||
"--timeout=30000",
|
||||
"--maxChars=none"
|
||||
]
|
||||
},
|
||||
"uiautomator2": {
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": [
|
||||
"C:\\Users\\mumur\\uiautomator2-mcp\\server.py"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
BIN
docs/screenshots/arsenal.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
docs/screenshots/autopwn.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
docs/screenshots/splash.png
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
imtaqin.id.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
17
index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0a0a0a" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
||||
<title>Pocket Pentester</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-32.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1793
package-lock.json
generated
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "pocketpentester",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/imtaqin.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
6
public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/tegalsec.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
screenshot/admin-finder.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
screenshot/auto-pwn.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
screenshot/dir-fuzz.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
screenshot/form-brute.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
screenshot/http-probe.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
screenshot/http-prove
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
screenshot/jwt.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
screenshot/jwtg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
screenshot/port-scan.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
screenshot/screenshot.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
screenshot/splash.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
screenshot/sqli-scan.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
screenshot/subdo-scan.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
screenshot/subdomain-takeover.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
screenshot/xploiter.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
screenshot/xss-scan.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
6321
src-tauri/Cargo.lock
generated
Normal file
48
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "pocketpentester"
|
||||
version = "0.1.0"
|
||||
description = "Mobile pentester toolkit"
|
||||
authors = ["imtaqin"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "pocketpentester_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "macros", "sync", "io-util", "fs"] }
|
||||
walkdir = "2"
|
||||
local-ip-address = "0.6"
|
||||
rustls = { version = "0.23", default-features = false, features = ["std", "ring"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
||||
x509-parser = "0.17"
|
||||
rustls-pki-types = "1"
|
||||
md5 = "0.7"
|
||||
futures = "0.3"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip", "json", "cookies"] }
|
||||
hickory-resolver = "0.24"
|
||||
anyhow = "1"
|
||||
regex = "1"
|
||||
once_cell = "1"
|
||||
url = "2"
|
||||
base64 = "0.22"
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
serde_yaml = "0.9"
|
||||
urlencoding = "2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
10
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
12
src-tauri/gen/android/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
src-tauri/gen/android/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
6
src-tauri/gen/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/src/main/**/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
70
src-tauri/gen/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,70 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "com.imtaqin.pocketpentester"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "com.imtaqin.pocketpentester"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
37
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.pocketpentester"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.imtaqin.pocketpentester
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.pocketpentester" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
10
src-tauri/gen/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Pocket Pentester</string>
|
||||
<string name="main_activity_title">Pocket Pentester</string>
|
||||
</resources>
|
||||
6
src-tauri/gen/android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.pocketpentester" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
22
src-tauri/gen/android/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
23
src-tauri/gen/android/buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """npm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src-tauri/gen/android/gradle.properties
Normal file
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
BIN
src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
185
src-tauri/gen/android/gradlew
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
src-tauri/gen/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
3
src-tauri/gen/android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
59
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
mod modules;
|
||||
|
||||
use modules::{admin_finder, autopwn, banner, dirfuzz, dns_tools, domain_grabber, form_brute, httpx, jwt, lan_map, port_scan, repeater, sqli, ssl_scan, subdomain, takeover, xploiter, xploiter_store, xss};
|
||||
|
||||
#[tauri::command]
|
||||
fn banner() -> &'static str {
|
||||
"Pocket Pentester v0.1 // offensive toolkit"
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn default_ports() -> Vec<u16> {
|
||||
port_scan::top_1000_ports()
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
banner,
|
||||
default_ports,
|
||||
port_scan::port_scan,
|
||||
subdomain::subdomain_enum,
|
||||
subdomain::subdomain_sources,
|
||||
httpx::http_probe,
|
||||
takeover::takeover_scan,
|
||||
sqli::sqli_scan,
|
||||
xss::xss_scan,
|
||||
jwt::jwt_analyze,
|
||||
xploiter::xploit_run,
|
||||
xploiter_store::xploit_store_init,
|
||||
xploiter_store::xploit_store_list,
|
||||
xploiter_store::xploit_store_read,
|
||||
xploiter_store::xploit_store_save,
|
||||
xploiter_store::xploit_store_delete,
|
||||
xploiter_store::xploit_store_duplicate,
|
||||
xploiter_store::xploit_store_starter_template,
|
||||
autopwn::autopwn_run,
|
||||
lan_map::lan_scan,
|
||||
lan_map::lan_local_info,
|
||||
repeater::repeater_send,
|
||||
repeater::repeater_to_curl,
|
||||
dirfuzz::dirfuzz_run,
|
||||
dirfuzz::dirfuzz_common_wordlist,
|
||||
admin_finder::admin_finder_run,
|
||||
admin_finder::admin_finder_wordlist,
|
||||
form_brute::form_brute_run,
|
||||
form_brute::form_brute_common_users,
|
||||
form_brute::form_brute_common_passwords,
|
||||
dns_tools::dns_query,
|
||||
ssl_scan::ssl_scan,
|
||||
banner::banner_grab,
|
||||
domain_grabber::iana_tld_list,
|
||||
domain_grabber::domain_grab,
|
||||
domain_grabber::wordlist_info,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
pocketpentester_lib::run()
|
||||
}
|
||||
312
src-tauri/src/modules/admin_finder.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
// ===================================================================
|
||||
// Admin Panel Finder — probe common admin/login paths, detect CMS.
|
||||
// Bundled with 450+ high-signal admin paths.
|
||||
// ===================================================================
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::stream::{self, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AdminFindRequest {
|
||||
pub base_url: String,
|
||||
#[serde(default)]
|
||||
pub extra_paths: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub use_builtin: bool,
|
||||
#[serde(default)]
|
||||
pub accept_status: Option<Vec<u16>>,
|
||||
pub concurrency: usize,
|
||||
pub timeout_ms: u64,
|
||||
#[serde(default)]
|
||||
pub follow_redirects: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AdminHit {
|
||||
pub url: String,
|
||||
pub status: u16,
|
||||
pub size: u64,
|
||||
pub title: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
pub login_form: bool,
|
||||
pub auth_header: Option<String>,
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
static TITLE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?is)<title[^>]*>(.*?)</title>").unwrap());
|
||||
static LOGIN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"(?is)<(?:input|form)[^>]*(?:type=['"]password['"]|name=['"]password['"]|name=['"]passwd['"])"#).unwrap());
|
||||
|
||||
fn detect_platform(body: &str, title: &Option<String>, hdrs: &reqwest::header::HeaderMap) -> Option<String> {
|
||||
let lo = body.to_lowercase();
|
||||
let t = title.clone().unwrap_or_default().to_lowercase();
|
||||
let powered = hdrs.get("x-powered-by").and_then(|v| v.to_str().ok()).unwrap_or("").to_lowercase();
|
||||
let server = hdrs.get("server").and_then(|v| v.to_str().ok()).unwrap_or("").to_lowercase();
|
||||
|
||||
if lo.contains("wp-login") || lo.contains("wp-submit") || t.contains("wordpress") { return Some("WordPress".into()); }
|
||||
if lo.contains("com_users") || lo.contains("joomla") || t.contains("joomla") { return Some("Joomla".into()); }
|
||||
if lo.contains("drupal-settings-json") || t.contains("drupal") { return Some("Drupal".into()); }
|
||||
if lo.contains("dashboardwidgets") || lo.contains("wp-admin") { return Some("WordPress admin".into()); }
|
||||
if t.contains("phpmyadmin") || lo.contains("phpmyadmin") { return Some("phpMyAdmin".into()); }
|
||||
if lo.contains("adminer") || t.contains("adminer") { return Some("Adminer".into()); }
|
||||
if t.contains("jenkins") || lo.contains("jenkins") { return Some("Jenkins".into()); }
|
||||
if t.contains("jira") { return Some("Jira".into()); }
|
||||
if t.contains("confluence") { return Some("Confluence".into()); }
|
||||
if t.contains("gitlab") { return Some("GitLab".into()); }
|
||||
if t.contains("grafana") || lo.contains("grafana") { return Some("Grafana".into()); }
|
||||
if t.contains("kibana") { return Some("Kibana".into()); }
|
||||
if t.contains("solr") { return Some("Apache Solr".into()); }
|
||||
if t.contains("tomcat") || lo.contains("apache tomcat") { return Some("Apache Tomcat".into()); }
|
||||
if t.contains("cpanel") { return Some("cPanel".into()); }
|
||||
if t.contains("plesk") { return Some("Plesk".into()); }
|
||||
if t.contains("directadmin") { return Some("DirectAdmin".into()); }
|
||||
if t.contains("webmin") { return Some("Webmin".into()); }
|
||||
if t.contains("mikrotik") { return Some("Mikrotik".into()); }
|
||||
if t.contains("pfsense") { return Some("pfSense".into()); }
|
||||
if t.contains("opnsense") { return Some("OPNsense".into()); }
|
||||
if powered.contains("asp.net") { return Some("ASP.NET".into()); }
|
||||
if powered.contains("php") { return Some("PHP".into()); }
|
||||
if server.contains("iis") { return Some("IIS".into()); }
|
||||
if server.contains("nginx") { return Some("nginx".into()); }
|
||||
if server.contains("apache") { return Some("Apache".into()); }
|
||||
None
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn admin_finder_wordlist() -> Vec<&'static str> {
|
||||
BUILTIN.to_vec()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn admin_finder_run(app: AppHandle, req: AdminFindRequest) -> Result<Vec<AdminHit>, String> {
|
||||
let base = req.base_url.trim_end_matches('/').to_string();
|
||||
let accept: HashSet<u16> = req.accept_status.clone()
|
||||
.unwrap_or_else(|| vec![200, 201, 301, 302, 307, 308, 401, 403])
|
||||
.into_iter().collect();
|
||||
|
||||
let mut paths: HashSet<String> = HashSet::new();
|
||||
if req.use_builtin {
|
||||
for p in BUILTIN.iter() { paths.insert(p.trim_start_matches('/').to_string()); }
|
||||
}
|
||||
for p in &req.extra_paths {
|
||||
let t = p.trim().trim_start_matches('/').to_string();
|
||||
if !t.is_empty() { paths.insert(t); }
|
||||
}
|
||||
if paths.is_empty() {
|
||||
return Err("no paths to probe (enable builtin or provide extra_paths)".into());
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(req.timeout_ms))
|
||||
.redirect(if req.follow_redirects {
|
||||
reqwest::redirect::Policy::limited(5)
|
||||
} else {
|
||||
reqwest::redirect::Policy::none()
|
||||
})
|
||||
.user_agent("Mozilla/5.0 (PocketPentester-AdminFinder)")
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let total = paths.len();
|
||||
let _ = app.emit("adminfind:status", format!("probing {total} admin path(s) on {base}"));
|
||||
let sem = Arc::new(Semaphore::new(req.concurrency.max(1)));
|
||||
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let client = Arc::new(client);
|
||||
|
||||
let hits: Vec<AdminHit> = stream::iter(paths.into_iter())
|
||||
.map(|p| {
|
||||
let base = base.clone();
|
||||
let client = client.clone();
|
||||
let sem = sem.clone();
|
||||
let done = done.clone();
|
||||
let accept = accept.clone();
|
||||
let app = app.clone();
|
||||
async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
let url = format!("{}/{}", base, p);
|
||||
let resp = match client.get(&url).send().await {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("adminfind:progress", serde_json::json!({"done": n, "total": total}));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("adminfind:progress", serde_json::json!({"done": n, "total": total}));
|
||||
|
||||
let status = resp.status().as_u16();
|
||||
if !accept.contains(&status) { return None; }
|
||||
|
||||
let hdrs = resp.headers().clone();
|
||||
let auth = hdrs.get("www-authenticate").and_then(|v| v.to_str().ok()).map(String::from);
|
||||
let redirect = hdrs.get("location").and_then(|v| v.to_str().ok()).map(String::from);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
let title = TITLE_RE.captures(&body)
|
||||
.and_then(|c| c.get(1))
|
||||
.map(|m| m.as_str().trim().chars().take(100).collect::<String>())
|
||||
.filter(|s| !s.is_empty());
|
||||
let login = LOGIN_RE.is_match(&body) || auth.is_some();
|
||||
let platform = detect_platform(&body, &title, &hdrs);
|
||||
|
||||
let hit = AdminHit {
|
||||
url,
|
||||
status,
|
||||
size: body.len() as u64,
|
||||
title,
|
||||
platform,
|
||||
login_form: login,
|
||||
auth_header: auth,
|
||||
redirect,
|
||||
};
|
||||
let _ = app.emit("adminfind:hit", hit.clone());
|
||||
Some(hit)
|
||||
}
|
||||
})
|
||||
.buffer_unordered(req.concurrency.max(1))
|
||||
.filter_map(|x| async move { x })
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let _ = app.emit("adminfind:done", hits.len());
|
||||
Ok(hits)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Built-in admin path wordlist (~320 high-signal paths)
|
||||
// ------------------------------------------------------------------
|
||||
const BUILTIN: &[&str] = &[
|
||||
// generic
|
||||
"admin", "admin/", "admin.php", "admin.html", "admin.asp", "admin.aspx", "admin.jsp",
|
||||
"admin/login", "admin/login.php", "admin/login.html", "admin/index.php", "admin/index.html",
|
||||
"admin/admin.php", "admin/home.php", "admin/home", "admin/dashboard", "admin/panel",
|
||||
"admin/admin_login", "admin/account.php", "admin/admin-login", "admin/controlpanel",
|
||||
"admin/controlpanel.html", "admin/cp.php", "admin/cp.html",
|
||||
"admin1", "admin1.php", "admin1.html", "admin2", "admin2.php", "admin2.html",
|
||||
"administrator", "administrator/", "administrator/index.php", "administrator/login.php",
|
||||
"administrator/account.php", "administrator/account.html",
|
||||
"adm", "adm/", "adm/index.php", "adm/admloginuser.php",
|
||||
"panel", "panel-administracion", "panel/", "controlpanel", "controlpanel/",
|
||||
"webadmin", "webadmin/", "webadmin/index.php", "webadmin/admin",
|
||||
"login", "login.php", "login.html", "login.asp", "login.aspx", "login.jsp",
|
||||
"log-in", "signin", "sign-in", "sign_in", "sign_up", "signup",
|
||||
"account", "account/login", "account.php", "accounts/login",
|
||||
"user/login", "users/sign_in", "users/login",
|
||||
"home.php", "home.html", "main.php", "main.html",
|
||||
|
||||
// wordpress
|
||||
"wp-admin", "wp-admin/", "wp-login.php", "wp-admin/admin-ajax.php",
|
||||
"wp-admin/install.php", "wp-admin/upgrade.php", "wp-admin/setup-config.php",
|
||||
"wp-content/plugins/", "wp-content/themes/", "wp-json/wp/v2/users",
|
||||
|
||||
// joomla
|
||||
"administrator/index.php", "administrator/components/",
|
||||
|
||||
// drupal
|
||||
"user", "user/login", "user/register", "admin/structure",
|
||||
|
||||
// magento
|
||||
"admin/admin", "admin/dashboard/", "magento_version",
|
||||
"index.php/admin/", "customer/account/login/",
|
||||
|
||||
// prestashop
|
||||
"modules/ps_shoppingcart",
|
||||
|
||||
// opencart
|
||||
"admin/index.php?route=common/login",
|
||||
|
||||
// laravel
|
||||
"login", "register", "password/reset", "horizon", "telescope",
|
||||
|
||||
// phpmyadmin / db admin
|
||||
"phpmyadmin", "phpmyadmin/", "phpmyadmin/index.php", "pma", "pma/", "pmamy", "pmamy2",
|
||||
"dbadmin", "dbadmin/", "mysql", "mysql/", "sqlmanager", "sqlmanager/",
|
||||
"adminer.php", "adminer/", "adminer", "phpMyAdmin/",
|
||||
|
||||
// cpanel / plesk / webmin
|
||||
"cpanel", "cpanel/", ":2082", ":2083",
|
||||
"plesk", "plesk/", ":8443",
|
||||
"webmin", "webmin/", ":10000",
|
||||
"directadmin", ":2222",
|
||||
"ispconfig", "vesta",
|
||||
|
||||
// tomcat / jboss / weblogic
|
||||
"manager/html", "manager/status", "host-manager/html",
|
||||
"jmx-console/", "web-console/", "admin-console/",
|
||||
"invoker/", "console/", "wls-wsat/CoordinatorPortType",
|
||||
|
||||
// elasticsearch / kibana / solr
|
||||
"_cluster/health", "_cat/indices", "_all",
|
||||
"kibana", "app/kibana",
|
||||
"solr/", "solr/admin/cores", "solr/#/",
|
||||
|
||||
// jenkins / gitlab / gitea
|
||||
"jenkins", "jenkins/", "jenkins/login",
|
||||
"gitlab/users/sign_in", "user/login",
|
||||
"admin/appearance",
|
||||
|
||||
// grafana / prometheus
|
||||
"grafana", "grafana/login",
|
||||
"prometheus", "prometheus/graph",
|
||||
|
||||
// rabbitmq / kafka ui
|
||||
"rabbitmq", ":15672", "#/queues",
|
||||
|
||||
// api platforms
|
||||
"api/admin", "api/administrator", "api/v1/admin", "api/login", "api/users",
|
||||
"graphql", "graphiql", "playground",
|
||||
"swagger", "swagger-ui", "swagger-ui.html", "swagger.json", "swagger/v1/swagger.json",
|
||||
"api-docs", "openapi.json", "openapi.yaml", "v2/api-docs",
|
||||
"actuator", "actuator/health", "actuator/env", "actuator/heapdump", "actuator/mappings",
|
||||
|
||||
// routers / network appliances
|
||||
"cgi-bin/luci", "cgi-bin/admin", "cgi-bin/login",
|
||||
"router", "router/login",
|
||||
|
||||
// mikrotik / pfsense / opnsense
|
||||
"webfig", "winbox",
|
||||
"system_advanced_admin.php", "diag_backup.php",
|
||||
|
||||
// misc control panels
|
||||
"dashboard", "dashboard/", "dashboard/login",
|
||||
"manage", "manage/", "manager/", "control", "control/", "siteadmin", "siteadmin/",
|
||||
"moderator", "moderator/", "operator/", "supervisor/",
|
||||
"console", "console/", "backend", "backend/", "staff", "staff/",
|
||||
"system", "system/", "system/login", "system/admin",
|
||||
"cms", "cms/", "cms/login",
|
||||
"portal", "portal/", "portal/login",
|
||||
|
||||
// auth endpoints
|
||||
"oauth", "oauth/authorize", "oauth2/authorize", "sso",
|
||||
".well-known/openid-configuration",
|
||||
|
||||
// file managers
|
||||
"files", "filemanager", "file_manager", "filemanager/", "files/login",
|
||||
|
||||
// backup / debug / test
|
||||
"test", "test/", "testing", "staging", "stage", "beta", "dev", "development",
|
||||
"old", "old/", "tmp", "temp", "cache", "backup", "backups",
|
||||
|
||||
// vendor-specific
|
||||
"owa", "owa/auth/logon.aspx", "ecp/default.aspx",
|
||||
"citrix", "vpn/index_access.html",
|
||||
"vcenter", "ui/", "vsphere-client/",
|
||||
"splunk", "en-US/account/login",
|
||||
"sonarqube", "sessions/new",
|
||||
"nexus", "artifactory",
|
||||
"harbor", "portainer",
|
||||
"rancher", "rancher/login",
|
||||
|
||||
// legacy / misc
|
||||
"home", "member", "members", "members/", "private", "private/",
|
||||
"secure", "secure/", "vip", "vip/",
|
||||
"webadm", "siteadm", "backoffice",
|
||||
];
|
||||
417
src-tauri/src/modules/autopwn.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
// ===================================================================
|
||||
// AutoPwn — one-shot orchestrator: subdomain → probe → exploit.
|
||||
//
|
||||
// Stage 1: passive crt.sh + optional DNS brute
|
||||
// Stage 2: live-host HTTP/S probe (concurrent)
|
||||
// Stage 3: xploit all selected templates on every alive host
|
||||
//
|
||||
// Emits `autopwn:*` events so the UI dashboard can track each stage
|
||||
// independently without colliding with standalone tool events.
|
||||
// ===================================================================
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::stream::{self, StreamExt};
|
||||
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use super::xploiter::{parse_templates, run_template_against_target, Finding};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AutopwnRequest {
|
||||
pub domain: String,
|
||||
pub use_passive: bool,
|
||||
pub brute_wordlist: Vec<String>,
|
||||
pub templates_yaml: Vec<String>,
|
||||
pub probe_ports: Option<Vec<u16>>,
|
||||
pub concurrency: usize,
|
||||
pub timeout_ms: u64,
|
||||
|
||||
/// Status codes considered "alive" for the exploit stage.
|
||||
/// Default: 200-299, 301, 302, 307, 308, 401, 403, 405.
|
||||
/// Explicitly rejects 400 (bad req / wrong SNI), 404 (no vhost),
|
||||
/// 5xx (backend dead).
|
||||
#[serde(default)]
|
||||
pub accept_status: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AutopwnSubHit {
|
||||
pub host: String,
|
||||
pub source: String,
|
||||
pub ips: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AutopwnProbeHit {
|
||||
pub url: String,
|
||||
pub status: u16,
|
||||
pub title: Option<String>,
|
||||
pub server: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AutopwnReport {
|
||||
pub domain: String,
|
||||
pub subdomains: Vec<AutopwnSubHit>,
|
||||
pub alive: Vec<AutopwnProbeHit>,
|
||||
pub findings: Vec<Finding>,
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// stage 1: subdomain enum
|
||||
// --------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct CrtRow { name_value: String }
|
||||
|
||||
async fn passive_crtsh(domain: &str) -> anyhow::Result<HashSet<String>> {
|
||||
let url = format!("https://crt.sh/?q=%25.{}&output=json", domain);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.user_agent("PocketPentester-AutoPwn/0.1")
|
||||
.build()?;
|
||||
let rows: Vec<CrtRow> = client.get(&url).send().await?.json().await?;
|
||||
let mut set = HashSet::new();
|
||||
for r in rows {
|
||||
for line in r.name_value.split('\n') {
|
||||
let host = line.trim().trim_start_matches("*.").to_lowercase();
|
||||
if host.ends_with(domain) && !host.is_empty() { set.insert(host); }
|
||||
}
|
||||
}
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
fn rand_label() -> String {
|
||||
use rand::Rng;
|
||||
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..16).map(|_| chars[rng.gen_range(0..chars.len())]).collect()
|
||||
}
|
||||
|
||||
/// Detect wildcard DNS by resolving 2 random non-existent subs.
|
||||
/// Returns the set of IPs that appear to be catch-all answers.
|
||||
async fn detect_wildcard(resolver: &TokioAsyncResolver, domain: &str) -> HashSet<String> {
|
||||
let mut ips: HashSet<String> = HashSet::new();
|
||||
for _ in 0..2 {
|
||||
let probe = format!("{}.{}", rand_label(), domain);
|
||||
if let Ok(lookup) = resolver.lookup_ip(probe.as_str()).await {
|
||||
for ip in lookup.iter() {
|
||||
ips.insert(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
ips
|
||||
}
|
||||
|
||||
async fn enum_subdomains(
|
||||
app: &AppHandle,
|
||||
domain: &str,
|
||||
use_passive: bool,
|
||||
brute: &[String],
|
||||
concurrency: usize,
|
||||
) -> Vec<AutopwnSubHit> {
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.timeout = Duration::from_secs(3);
|
||||
opts.attempts = 1;
|
||||
let resolver = Arc::new(TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), opts));
|
||||
|
||||
// wildcard detection BEFORE brute
|
||||
let wildcard = detect_wildcard(&resolver, domain).await;
|
||||
if !wildcard.is_empty() {
|
||||
let _ = app.emit(
|
||||
"autopwn:status",
|
||||
format!("wildcard DNS detected ({} IPs) — results pointing only to these will be filtered",
|
||||
wildcard.iter().cloned().collect::<Vec<_>>().join(","))
|
||||
);
|
||||
}
|
||||
|
||||
let mut candidates: HashSet<(String, String)> = HashSet::new();
|
||||
candidates.insert((domain.to_string(), "root".into()));
|
||||
|
||||
if use_passive {
|
||||
let _ = app.emit("autopwn:stage", "recon:passive");
|
||||
match passive_crtsh(domain).await {
|
||||
Ok(hosts) => {
|
||||
let _ = app.emit("autopwn:status", format!("crt.sh: +{} subs", hosts.len()));
|
||||
for h in hosts { candidates.insert((h, "crtsh".into())); }
|
||||
}
|
||||
Err(e) => { let _ = app.emit("autopwn:status", format!("crt.sh error: {e}")); }
|
||||
}
|
||||
}
|
||||
|
||||
for w in brute {
|
||||
candidates.insert((format!("{}.{}", w.trim(), domain), "brute".into()));
|
||||
}
|
||||
|
||||
let total = candidates.len();
|
||||
let _ = app.emit("autopwn:stage", "recon:resolve");
|
||||
let _ = app.emit("autopwn:status", format!("resolving {total} candidates"));
|
||||
|
||||
let sem = Arc::new(Semaphore::new(concurrency.max(1)));
|
||||
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
let wildcard_arc = Arc::new(wildcard);
|
||||
|
||||
let hits: Vec<AutopwnSubHit> = stream::iter(candidates.into_iter())
|
||||
.map(|(host, source)| {
|
||||
let resolver = resolver.clone();
|
||||
let sem = sem.clone();
|
||||
let done = done.clone();
|
||||
let app = app.clone();
|
||||
let wildcard = wildcard_arc.clone();
|
||||
async move {
|
||||
let _p = sem.acquire().await.unwrap();
|
||||
let result = resolver.lookup_ip(host.as_str()).await;
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("autopwn:progress", serde_json::json!({
|
||||
"stage": "recon", "done": n, "total": total
|
||||
}));
|
||||
match result {
|
||||
Ok(lookup) => {
|
||||
let ips: Vec<String> = lookup.iter().map(|ip| ip.to_string()).collect();
|
||||
if ips.is_empty() { return None; }
|
||||
|
||||
// wildcard filter: skip if ALL ips match the wildcard set
|
||||
// (keep root domain regardless)
|
||||
if !wildcard.is_empty() && source != "root"
|
||||
&& ips.iter().all(|ip| wildcard.contains(ip))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let hit = AutopwnSubHit { host, source, ips };
|
||||
let _ = app.emit("autopwn:sub", hit.clone());
|
||||
Some(hit)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(concurrency.max(1))
|
||||
.filter_map(|x| async move { x })
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
hits
|
||||
}
|
||||
|
||||
/// Default HTTP status codes treated as "alive & worth exploiting".
|
||||
fn default_accept_statuses() -> Vec<u16> {
|
||||
vec![
|
||||
200, 201, 202, 203, 204, 206,
|
||||
301, 302, 303, 307, 308,
|
||||
401, 403, 405, 418, // 418 = sometimes WAF, but still shows alive service
|
||||
]
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// stage 2: HTTP probe
|
||||
// --------------------------------------------------------------
|
||||
|
||||
async fn probe_hosts(
|
||||
app: &AppHandle,
|
||||
hosts: &[AutopwnSubHit],
|
||||
ports: &[u16],
|
||||
concurrency: usize,
|
||||
timeout_ms: u64,
|
||||
accept_status: &[u16],
|
||||
) -> Vec<AutopwnProbeHit> {
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(timeout_ms))
|
||||
.redirect(reqwest::redirect::Policy::limited(3))
|
||||
.user_agent("Mozilla/5.0 (PocketPentester-AutoPwn)")
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new());
|
||||
|
||||
let mut urls: Vec<String> = Vec::new();
|
||||
for h in hosts {
|
||||
for p in ports {
|
||||
let scheme = if matches!(p, 443 | 8443) { "https" } else { "http" };
|
||||
if *p == 80 || *p == 443 {
|
||||
urls.push(format!("{scheme}://{}", h.host));
|
||||
} else {
|
||||
urls.push(format!("{scheme}://{}:{}", h.host, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total = urls.len();
|
||||
let _ = app.emit("autopwn:stage", "probe");
|
||||
let _ = app.emit("autopwn:status", format!("probing {total} urls"));
|
||||
|
||||
let sem = Arc::new(Semaphore::new(concurrency.max(1)));
|
||||
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let client = Arc::new(client);
|
||||
|
||||
let title_re = regex::Regex::new(r"(?is)<title[^>]*>(.*?)</title>").unwrap();
|
||||
// regex that recognizes "nothing useful" responses — default pages, error placeholders, etc.
|
||||
let junk_re = regex::Regex::new(
|
||||
r"(?i)(?:welcome to nginx|it works|apache2 ubuntu default page|test page for the (?:apache|nginx)|site not found|default backend - 404|403 forbidden</center>|bad request</h1>|invalid hostname)"
|
||||
).unwrap();
|
||||
let accept: HashSet<u16> = accept_status.iter().copied().collect();
|
||||
|
||||
let alive: Vec<AutopwnProbeHit> = stream::iter(urls.into_iter())
|
||||
.map(|url| {
|
||||
let sem = sem.clone();
|
||||
let done = done.clone();
|
||||
let client = client.clone();
|
||||
let app = app.clone();
|
||||
let title_re = title_re.clone();
|
||||
let junk_re = junk_re.clone();
|
||||
let accept = accept.clone();
|
||||
async move {
|
||||
let _p = sem.acquire().await.unwrap();
|
||||
let result = client.get(&url).send().await;
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("autopwn:progress", serde_json::json!({
|
||||
"stage": "probe", "done": n, "total": total
|
||||
}));
|
||||
let resp = match result { Ok(r) => r, Err(_) => return None };
|
||||
let status = resp.status().as_u16();
|
||||
|
||||
// ---- status whitelist ----
|
||||
if !accept.contains(&status) {
|
||||
let _ = app.emit("autopwn:status",
|
||||
format!("skip {} ({}): status not in accept-list", url, status));
|
||||
return None;
|
||||
}
|
||||
|
||||
let server = resp.headers().get("server")
|
||||
.and_then(|v| v.to_str().ok()).map(String::from);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
|
||||
// ---- junk-body filter ----
|
||||
if junk_re.is_match(&body) {
|
||||
let _ = app.emit("autopwn:status",
|
||||
format!("skip {} ({}): default/placeholder page", url, status));
|
||||
return None;
|
||||
}
|
||||
|
||||
let title = title_re.captures(&body)
|
||||
.and_then(|c| c.get(1))
|
||||
.map(|m| m.as_str().trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let hit = AutopwnProbeHit { url, status, title, server };
|
||||
let _ = app.emit("autopwn:alive", hit.clone());
|
||||
Some(hit)
|
||||
}
|
||||
})
|
||||
.buffer_unordered(concurrency.max(1))
|
||||
.filter_map(|x| async move { x })
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
alive
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// stage 3: exploit
|
||||
// --------------------------------------------------------------
|
||||
|
||||
async fn exploit_alive(
|
||||
app: &AppHandle,
|
||||
alive: &[AutopwnProbeHit],
|
||||
templates_yaml: &[String],
|
||||
concurrency: usize,
|
||||
timeout_ms: u64,
|
||||
) -> Vec<Finding> {
|
||||
let templates = parse_templates(templates_yaml, app, "autopwn:status");
|
||||
if templates.is_empty() { return Vec::new(); }
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(timeout_ms))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.user_agent("Mozilla/5.0 (PocketPentester-AutoPwn)")
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new());
|
||||
|
||||
let tasks: Vec<(String, super::xploiter::Template)> = alive.iter()
|
||||
.flat_map(|a| templates.iter().map(move |t| (a.url.clone(), t.clone())))
|
||||
.collect();
|
||||
let total = tasks.len();
|
||||
let _ = app.emit("autopwn:stage", "exploit");
|
||||
let _ = app.emit("autopwn:status", format!("exploiting: {total} target×template combos"));
|
||||
|
||||
let sem = Arc::new(Semaphore::new(concurrency.max(1)));
|
||||
let client = Arc::new(client);
|
||||
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
stream::iter(tasks.into_iter())
|
||||
.map(|(target, tpl)| {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let done = done.clone();
|
||||
let app = app.clone();
|
||||
async move {
|
||||
let _p = sem.acquire().await.unwrap();
|
||||
let v = run_template_against_target(&client, &target, &tpl, &app, "autopwn:xpl").await;
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("autopwn:progress", serde_json::json!({
|
||||
"stage": "exploit", "done": n, "total": total
|
||||
}));
|
||||
v
|
||||
}
|
||||
})
|
||||
.buffer_unordered(concurrency.max(1))
|
||||
.flat_map(|v| stream::iter(v.into_iter()))
|
||||
.collect()
|
||||
.await
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// command
|
||||
// --------------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn autopwn_run(app: AppHandle, req: AutopwnRequest) -> Result<AutopwnReport, String> {
|
||||
let ports = req.probe_ports.clone().unwrap_or_else(|| vec![80, 443, 8080, 8443]);
|
||||
let accept_status = req.accept_status.clone().unwrap_or_else(default_accept_statuses);
|
||||
|
||||
// stage 1 ---------------------------------------------------
|
||||
let subs = enum_subdomains(&app, &req.domain, req.use_passive, &req.brute_wordlist, req.concurrency).await;
|
||||
let _ = app.emit("autopwn:stage-done", serde_json::json!({ "stage": "recon", "count": subs.len() }));
|
||||
|
||||
if subs.is_empty() {
|
||||
return Err("no live subdomains resolved".into());
|
||||
}
|
||||
|
||||
// stage 2 ---------------------------------------------------
|
||||
let alive = probe_hosts(&app, &subs, &ports, req.concurrency, req.timeout_ms, &accept_status).await;
|
||||
let _ = app.emit("autopwn:stage-done", serde_json::json!({ "stage": "probe", "count": alive.len() }));
|
||||
|
||||
if alive.is_empty() {
|
||||
let _ = app.emit("autopwn:done", 0);
|
||||
return Ok(AutopwnReport {
|
||||
domain: req.domain,
|
||||
subdomains: subs, alive: vec![], findings: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// stage 3 ---------------------------------------------------
|
||||
let findings = if req.templates_yaml.is_empty() {
|
||||
let _ = app.emit("autopwn:status", "no templates selected — skipping exploit stage");
|
||||
Vec::new()
|
||||
} else {
|
||||
exploit_alive(&app, &alive, &req.templates_yaml, req.concurrency, req.timeout_ms).await
|
||||
};
|
||||
|
||||
let _ = app.emit("autopwn:stage-done", serde_json::json!({
|
||||
"stage": "exploit", "count": findings.len()
|
||||
}));
|
||||
let _ = app.emit("autopwn:done", findings.len());
|
||||
|
||||
Ok(AutopwnReport {
|
||||
domain: req.domain,
|
||||
subdomains: subs,
|
||||
alive,
|
||||
findings,
|
||||
})
|
||||
}
|
||||
194
src-tauri/src/modules/banner.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
// ===================================================================
|
||||
// Banner Grabber — TCP connect + read service greeting, fingerprint
|
||||
// common services (SSH / FTP / SMTP / HTTP / Redis / MySQL / IRC).
|
||||
// ===================================================================
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::stream::{self, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct BannerRequest {
|
||||
pub host: String,
|
||||
pub ports: Vec<u16>,
|
||||
#[serde(default = "default_conc")]
|
||||
pub concurrency: usize,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
fn default_conc() -> usize { 50 }
|
||||
fn default_timeout() -> u64 { 4000 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BannerHit {
|
||||
pub port: u16,
|
||||
pub banner: String,
|
||||
pub service: String,
|
||||
pub version: Option<String>,
|
||||
pub bytes: usize,
|
||||
}
|
||||
|
||||
fn fingerprint(port: u16, banner: &str) -> (String, Option<String>) {
|
||||
let lo = banner.to_lowercase();
|
||||
let first_line = banner.lines().next().unwrap_or("").to_string();
|
||||
|
||||
// SSH
|
||||
if banner.starts_with("SSH-") {
|
||||
let ver = first_line.trim().to_string();
|
||||
return ("SSH".into(), Some(ver));
|
||||
}
|
||||
|
||||
// HTTP (we sent a HEAD /)
|
||||
if banner.starts_with("HTTP/") {
|
||||
let server = banner.lines()
|
||||
.find(|l| l.to_lowercase().starts_with("server:"))
|
||||
.map(|l| l.trim_start_matches("Server:").trim_start_matches("server:").trim().to_string());
|
||||
return ("HTTP".into(), server);
|
||||
}
|
||||
|
||||
// FTP
|
||||
if banner.starts_with("220 ") && (lo.contains("ftp") || port == 21) {
|
||||
let ver = first_line.trim_start_matches("220 ").trim().to_string();
|
||||
return ("FTP".into(), Some(ver));
|
||||
}
|
||||
|
||||
// SMTP
|
||||
if banner.starts_with("220 ") && (lo.contains("smtp") || lo.contains("mail") || port == 25 || port == 587) {
|
||||
return ("SMTP".into(), Some(first_line));
|
||||
}
|
||||
|
||||
// POP3 / IMAP
|
||||
if banner.starts_with("+OK") { return ("POP3".into(), Some(first_line)); }
|
||||
if banner.starts_with("* OK") { return ("IMAP".into(), Some(first_line)); }
|
||||
|
||||
// Telnet
|
||||
if banner.as_bytes().first() == Some(&0xFF) { return ("Telnet".into(), None); }
|
||||
|
||||
// Redis
|
||||
if banner.starts_with("+PONG") || banner.starts_with("-NOAUTH") { return ("Redis".into(), None); }
|
||||
if banner.starts_with("-ERR") && port == 6379 { return ("Redis".into(), Some(first_line)); }
|
||||
|
||||
// MySQL (handshake starts with packet length + protocol 0x0a)
|
||||
if banner.as_bytes().len() >= 5 && banner.as_bytes()[4] == 0x0a && port == 3306 {
|
||||
let ver_start = 5;
|
||||
let ver_end = banner.as_bytes().iter().skip(ver_start).position(|&b| b == 0)
|
||||
.map(|p| ver_start + p).unwrap_or(banner.len());
|
||||
if ver_end > ver_start {
|
||||
return ("MySQL".into(), Some(String::from_utf8_lossy(&banner.as_bytes()[ver_start..ver_end]).to_string()));
|
||||
}
|
||||
return ("MySQL".into(), None);
|
||||
}
|
||||
|
||||
// PostgreSQL — responds with error on junk; check for "FATAL" or specific msg
|
||||
if port == 5432 && (lo.contains("fatal") || lo.contains("postgresql")) {
|
||||
return ("PostgreSQL".into(), None);
|
||||
}
|
||||
|
||||
// RDP — first byte 0x03 (TPKT)
|
||||
if banner.as_bytes().first() == Some(&0x03) && port == 3389 {
|
||||
return ("RDP".into(), None);
|
||||
}
|
||||
|
||||
// VNC
|
||||
if banner.starts_with("RFB ") { return ("VNC".into(), Some(first_line.trim().into())); }
|
||||
|
||||
// IRC
|
||||
if banner.starts_with(":") && lo.contains("irc") { return ("IRC".into(), Some(first_line)); }
|
||||
|
||||
// MongoDB — ismaster reply; harder
|
||||
if port == 27017 { return ("MongoDB".into(), None); }
|
||||
|
||||
// Fallback by port
|
||||
let by_port = match port {
|
||||
53 => "DNS", 22 => "SSH?", 23 => "Telnet?", 25 => "SMTP?",
|
||||
80 | 8080 | 8000 | 8088 => "HTTP?", 110 => "POP3?", 143 => "IMAP?",
|
||||
443 | 8443 => "HTTPS?", 445 => "SMB", 465 => "SMTPS",
|
||||
587 => "SMTP-SUB", 993 => "IMAPS", 995 => "POP3S",
|
||||
1433 => "MSSQL", 1521 => "Oracle", 2049 => "NFS",
|
||||
3306 => "MySQL", 3389 => "RDP", 5432 => "PostgreSQL",
|
||||
5900 => "VNC", 6379 => "Redis", 6667 => "IRC",
|
||||
9200 => "Elasticsearch",
|
||||
11211 => "Memcached", 27017 => "MongoDB",
|
||||
_ => "unknown",
|
||||
};
|
||||
(by_port.to_string(), None)
|
||||
}
|
||||
|
||||
async fn grab(host: &str, port: u16, timeout_ms: u64) -> Option<BannerHit> {
|
||||
let addr = format!("{host}:{port}");
|
||||
let stream = timeout(Duration::from_millis(timeout_ms), TcpStream::connect(&addr)).await.ok()?.ok()?;
|
||||
let _ = stream.set_nodelay(true);
|
||||
let mut stream = stream;
|
||||
|
||||
// For HTTP-ish ports, send a HEAD request to elicit a Server header
|
||||
let triggers: &[&[u8]] = match port {
|
||||
80 | 8080 | 8000 | 8008 | 8088 | 8888 | 9000 | 9090 | 10000 =>
|
||||
&[b"HEAD / HTTP/1.0\r\n\r\n"],
|
||||
21 | 22 | 25 | 110 | 143 | 220 | 5432 | 6379 => &[],
|
||||
_ => &[],
|
||||
};
|
||||
for t in triggers {
|
||||
let _ = timeout(Duration::from_millis(500), stream.write_all(t)).await;
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 2048];
|
||||
let read = timeout(Duration::from_millis(timeout_ms), stream.read(&mut buf)).await.ok()?.ok()?;
|
||||
if read == 0 { return None; }
|
||||
|
||||
let banner_raw = String::from_utf8_lossy(&buf[..read]).to_string();
|
||||
let banner = banner_raw.trim_end_matches(['\r', '\n', '\0']).to_string();
|
||||
let (service, version) = fingerprint(port, &banner);
|
||||
|
||||
// If banner is empty but we got bytes (binary), keep as-is
|
||||
let display = if banner.is_empty() && read > 0 {
|
||||
format!("[binary {} bytes]", read)
|
||||
} else {
|
||||
// strip non-printable for display
|
||||
banner.chars().map(|c| if c.is_ascii_graphic() || c == ' ' || c == '\n' { c } else { '.' }).collect::<String>()
|
||||
};
|
||||
|
||||
Some(BannerHit { port, banner: display, service, version, bytes: read })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn banner_grab(app: AppHandle, req: BannerRequest) -> Result<Vec<BannerHit>, String> {
|
||||
let total = req.ports.len();
|
||||
let _ = app.emit("banner:status", format!("grabbing banners on {} port(s)", total));
|
||||
|
||||
let sem = Arc::new(Semaphore::new(req.concurrency.max(1)));
|
||||
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let host = Arc::new(req.host.clone());
|
||||
|
||||
let hits: Vec<BannerHit> = stream::iter(req.ports.into_iter())
|
||||
.map(|port| {
|
||||
let sem = sem.clone();
|
||||
let done = done.clone();
|
||||
let app = app.clone();
|
||||
let host = host.clone();
|
||||
async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
let res = grab(&host, port, req.timeout_ms).await;
|
||||
let n = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("banner:progress", serde_json::json!({"done": n, "total": total}));
|
||||
if let Some(ref h) = res {
|
||||
let _ = app.emit("banner:hit", h.clone());
|
||||
}
|
||||
res
|
||||
}
|
||||
})
|
||||
.buffer_unordered(req.concurrency.max(1))
|
||||
.filter_map(|x| async move { x })
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let _ = app.emit("banner:done", hits.len());
|
||||
Ok(hits)
|
||||
}
|
||||
283
src-tauri/src/modules/dirfuzz.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
// ===================================================================
|
||||
// Directory Fuzzer — feroxbuster-style content discovery.
|
||||
// Streams hits via events. Supports extensions, recursion, filters.
|
||||
// ===================================================================
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::stream::{self, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DirFuzzRequest {
|
||||
pub base_url: String,
|
||||
pub wordlist: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub extensions: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub accept_status: Option<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
pub size_min: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub size_max: Option<u64>,
|
||||
pub concurrency: usize,
|
||||
pub timeout_ms: u64,
|
||||
#[serde(default)]
|
||||
pub follow_redirects: bool,
|
||||
#[serde(default)]
|
||||
pub recursive: bool,
|
||||
#[serde(default = "default_depth")]
|
||||
pub recursion_depth: u8,
|
||||
#[serde(default)]
|
||||
pub headers: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
fn default_depth() -> u8 { 2 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DirHit {
|
||||
pub url: String,
|
||||
pub status: u16,
|
||||
pub size: u64,
|
||||
pub words: usize,
|
||||
pub lines: usize,
|
||||
pub redirect: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub time_ms: u128,
|
||||
}
|
||||
|
||||
fn default_statuses() -> Vec<u16> {
|
||||
vec![200, 201, 204, 301, 302, 307, 308, 401, 403, 405]
|
||||
}
|
||||
|
||||
fn build_client(req: &DirFuzzRequest) -> reqwest::Client {
|
||||
let ua = req.user_agent.clone()
|
||||
.unwrap_or_else(|| "Mozilla/5.0 (PocketPentester-DirFuzz)".into());
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(req.timeout_ms))
|
||||
.redirect(if req.follow_redirects {
|
||||
reqwest::redirect::Policy::limited(5)
|
||||
} else {
|
||||
reqwest::redirect::Policy::none()
|
||||
})
|
||||
.user_agent(ua)
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new())
|
||||
}
|
||||
|
||||
fn expand_paths(word: &str, extensions: &[String]) -> Vec<String> {
|
||||
let w = word.trim().trim_start_matches('/');
|
||||
if w.is_empty() { return vec![]; }
|
||||
let mut out = vec![w.to_string()];
|
||||
for ext in extensions {
|
||||
let e = ext.trim();
|
||||
if e.is_empty() { continue; }
|
||||
let normalized = if e.starts_with('.') { e.to_string() } else { format!(".{e}") };
|
||||
out.push(format!("{w}{normalized}"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_title(body: &str) -> Option<String> {
|
||||
let lower = body.to_lowercase();
|
||||
let start = lower.find("<title")?;
|
||||
let rest = &body[start..];
|
||||
let body_start = rest.find('>')? + 1;
|
||||
let end = lower[start..].find("</title>")?;
|
||||
let title = &rest[body_start..end];
|
||||
let t = title.trim();
|
||||
if t.is_empty() { None } else { Some(t.chars().take(100).collect()) }
|
||||
}
|
||||
|
||||
async fn probe(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
headers: &HashMap<String, String>,
|
||||
) -> Option<DirHit> {
|
||||
let start = std::time::Instant::now();
|
||||
let mut builder = client.get(url);
|
||||
for (k, v) in headers {
|
||||
if k.trim().is_empty() { continue; }
|
||||
builder = builder.header(k.trim(), v);
|
||||
}
|
||||
let resp = builder.send().await.ok()?;
|
||||
let status = resp.status().as_u16();
|
||||
let content_type = resp.headers().get("content-type")
|
||||
.and_then(|v| v.to_str().ok()).map(String::from);
|
||||
let redirect = resp.headers().get("location")
|
||||
.and_then(|v| v.to_str().ok()).map(String::from);
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
let time_ms = start.elapsed().as_millis();
|
||||
|
||||
Some(DirHit {
|
||||
url: url.to_string(),
|
||||
status,
|
||||
size: body.len() as u64,
|
||||
words: body.split_whitespace().count(),
|
||||
lines: body.lines().count(),
|
||||
redirect,
|
||||
title: parse_title(&body),
|
||||
content_type,
|
||||
time_ms,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dirfuzz_run(app: AppHandle, req: DirFuzzRequest) -> Result<Vec<DirHit>, String> {
|
||||
let base = req.base_url.trim_end_matches('/').to_string();
|
||||
let client = Arc::new(build_client(&req));
|
||||
let accept: Vec<u16> = req.accept_status.clone().unwrap_or_else(default_statuses);
|
||||
let accept_set: std::collections::HashSet<u16> = accept.iter().copied().collect();
|
||||
|
||||
// ---- baseline calibration: fetch a random non-existent path to detect wildcard 200s ----
|
||||
let wildcard_marker = format!("__xploit_wildcard_{}_{}", rand::random::<u32>(), rand::random::<u32>());
|
||||
let wildcard_url = format!("{}/{}", base, wildcard_marker);
|
||||
let wildcard_size: Option<u64> = probe(&client, &wildcard_url, &req.headers).await
|
||||
.filter(|h| accept_set.contains(&h.status))
|
||||
.map(|h| h.size);
|
||||
if let Some(sz) = wildcard_size {
|
||||
let _ = app.emit("dirfuzz:status",
|
||||
format!("wildcard calibration: {} returned status (size={}) — same-size responses will be filtered", wildcard_url, sz));
|
||||
}
|
||||
|
||||
// ---- build initial path list ----
|
||||
let mut initial_paths: Vec<String> = Vec::new();
|
||||
for w in &req.wordlist {
|
||||
for p in expand_paths(w, &req.extensions) {
|
||||
initial_paths.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
let queue: Arc<Mutex<Vec<(String, u8)>>> = Arc::new(Mutex::new(vec![(base.clone(), 0)]));
|
||||
let all_hits: Arc<Mutex<Vec<DirHit>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let total_done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
loop {
|
||||
let (current_base, depth) = {
|
||||
let mut q = queue.lock().await;
|
||||
if q.is_empty() { break; }
|
||||
q.remove(0)
|
||||
};
|
||||
|
||||
let urls: Vec<String> = initial_paths.iter()
|
||||
.map(|p| format!("{}/{}", current_base.trim_end_matches('/'), p))
|
||||
.collect();
|
||||
let total = urls.len();
|
||||
|
||||
let _ = app.emit("dirfuzz:status",
|
||||
format!("fuzzing {} (depth {}): {} paths", current_base, depth, total));
|
||||
|
||||
let sem = Arc::new(Semaphore::new(req.concurrency.max(1)));
|
||||
let local_hits: Arc<Mutex<Vec<DirHit>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
stream::iter(urls.into_iter())
|
||||
.map(|url| {
|
||||
let client = client.clone();
|
||||
let sem = sem.clone();
|
||||
let headers = req.headers.clone();
|
||||
let accept_set = accept_set.clone();
|
||||
let size_min = req.size_min;
|
||||
let size_max = req.size_max;
|
||||
let wildcard_size = wildcard_size;
|
||||
let total_done = total_done.clone();
|
||||
let local_hits = local_hits.clone();
|
||||
let all_hits_c = all_hits.clone();
|
||||
let app = app.clone();
|
||||
async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
if let Some(hit) = probe(&client, &url, &headers).await {
|
||||
let n = total_done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
let _ = app.emit("dirfuzz:progress",
|
||||
serde_json::json!({"done": n, "total_seen": total_done.load(std::sync::atomic::Ordering::Relaxed)}));
|
||||
|
||||
if !accept_set.contains(&hit.status) { return; }
|
||||
if let Some(min) = size_min { if hit.size < min { return; } }
|
||||
if let Some(max) = size_max { if hit.size > max { return; } }
|
||||
if let Some(wsz) = wildcard_size { if hit.size == wsz { return; } }
|
||||
|
||||
let _ = app.emit("dirfuzz:hit", hit.clone());
|
||||
local_hits.lock().await.push(hit.clone());
|
||||
all_hits_c.lock().await.push(hit);
|
||||
} else {
|
||||
total_done.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(req.concurrency.max(1))
|
||||
.for_each(|_| async {})
|
||||
.await;
|
||||
|
||||
// recursion: enqueue hits that look like directories
|
||||
if req.recursive && depth < req.recursion_depth {
|
||||
let hits = local_hits.lock().await;
|
||||
for h in hits.iter() {
|
||||
let looks_like_dir = h.status == 301 || h.status == 302
|
||||
|| h.url.ends_with('/')
|
||||
|| h.redirect.as_deref().map(|r| r.ends_with('/')).unwrap_or(false);
|
||||
if looks_like_dir {
|
||||
let mut q = queue.lock().await;
|
||||
q.push((h.url.trim_end_matches('/').to_string(), depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hits = all_hits.lock().await.clone();
|
||||
let _ = app.emit("dirfuzz:done", hits.len());
|
||||
Ok(hits)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dirfuzz_common_wordlist() -> Vec<String> {
|
||||
// compact default wordlist — 150 of the highest-signal paths
|
||||
vec![
|
||||
"admin", "administrator", "login", "signin", "signup", "register",
|
||||
"dashboard", "api", "api/v1", "api/v2", "api/v3", "graphql",
|
||||
"robots.txt", "sitemap.xml", "crossdomain.xml", "clientaccesspolicy.xml",
|
||||
".git/config", ".git/HEAD", ".git/logs/HEAD", ".svn/entries", ".hg/hgrc",
|
||||
".env", ".env.local", ".env.production", ".env.backup",
|
||||
"config", "config.php", "config.json", "config.yml", "config.xml",
|
||||
"backup", "backup.zip", "backup.tar.gz", "backup.sql", "backup.bak",
|
||||
"db.sql", "dump.sql", "database.sql", "site.sql",
|
||||
"test", "test.php", "test.html", "phpinfo.php", "info.php",
|
||||
"server-status", "server-info", "status",
|
||||
"wp-admin", "wp-login.php", "wp-content", "wp-includes", "wp-config.php",
|
||||
"administrator/index.php", "user/login", "users/sign_in",
|
||||
"phpmyadmin", "myadmin", "mysql", "dbadmin", "adminer", "adminer.php",
|
||||
".htaccess", ".htpasswd", ".DS_Store", "thumbs.db",
|
||||
"uploads", "upload", "files", "file", "download", "downloads",
|
||||
"images", "img", "assets", "static", "public", "private",
|
||||
"docs", "doc", "documentation", "swagger", "swagger.json",
|
||||
"swagger-ui.html", "api-docs", "openapi.json", "openapi.yaml",
|
||||
"console", "actuator", "actuator/health", "actuator/env", "actuator/info",
|
||||
"metrics", "prometheus", "health", "healthcheck", "readiness", "liveness",
|
||||
"debug", "trace", "error", "errors", "log", "logs", "error_log",
|
||||
"jenkins", "phpunit", "webdav", "manager/html", "tomcat",
|
||||
"portal", "support", "help", "about", "contact", "feedback",
|
||||
"mail", "email", "webmail", "smtp", "imap",
|
||||
"vpn", "sso", "auth", "oauth", "oauth2", "openid",
|
||||
".well-known/security.txt", ".well-known/openid-configuration",
|
||||
".well-known/acme-challenge", ".well-known/nodeinfo",
|
||||
"index.php", "index.html", "index.asp", "index.aspx", "index.jsp",
|
||||
"home", "main", "default",
|
||||
"secret", "private.key", "id_rsa", "id_dsa",
|
||||
"setup", "install", "installer", "setup.php",
|
||||
"cgi-bin", "cgi-bin/test.sh", "cgi-bin/info.sh",
|
||||
"dev", "development", "staging", "stage", "beta", "qa", "uat",
|
||||
"old", "new", "tmp", "temp", "cache",
|
||||
"api/users", "api/user", "api/me", "api/login", "api/admin",
|
||||
"admin/login", "admin/config", "admin/users",
|
||||
"fckeditor", "ckeditor", "tinymce", "editor",
|
||||
"vendor", "node_modules", "composer.json", "composer.lock",
|
||||
"package.json", "yarn.lock", "webpack.config.js",
|
||||
"wp-json", "wp-json/wp/v2", "wp-json/wp/v2/users",
|
||||
].into_iter().map(String::from).collect()
|
||||
}
|
||||
263
src-tauri/src/modules/dns_tools.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
// ===================================================================
|
||||
// DNS Tools — comprehensive record lookup + zone transfer + DNSSEC
|
||||
// ===================================================================
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::proto::rr::RecordType;
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DnsRequest {
|
||||
pub domain: String,
|
||||
#[serde(default)]
|
||||
pub resolver: Option<String>, // e.g. "1.1.1.1:53", "8.8.8.8:53", "internal-dns.corp:53"
|
||||
#[serde(default)]
|
||||
pub types: Option<Vec<String>>, // subset to query, None = all
|
||||
#[serde(default)]
|
||||
pub try_axfr: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DnsReport {
|
||||
pub domain: String,
|
||||
pub records: Vec<DnsRecordGroup>,
|
||||
pub axfr: Option<AxfrResult>,
|
||||
pub dnssec: DnssecCheck,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DnsRecordGroup {
|
||||
pub rtype: String,
|
||||
pub values: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AxfrResult {
|
||||
pub success: bool,
|
||||
pub nameserver: String,
|
||||
pub records_dumped: Vec<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DnssecCheck {
|
||||
pub dnskey_count: usize,
|
||||
pub has_dnssec: bool,
|
||||
}
|
||||
|
||||
fn build_resolver(custom: Option<&str>) -> Result<TokioAsyncResolver, String> {
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.timeout = Duration::from_secs(4);
|
||||
opts.attempts = 2;
|
||||
|
||||
if let Some(addr) = custom {
|
||||
let sa: SocketAddr = if addr.contains(':') {
|
||||
addr.parse().map_err(|e| format!("bad resolver addr: {e}"))?
|
||||
} else {
|
||||
format!("{addr}:53").parse().map_err(|e| format!("bad resolver addr: {e}"))?
|
||||
};
|
||||
let mut cfg = ResolverConfig::new();
|
||||
cfg.add_name_server(NameServerConfig {
|
||||
socket_addr: sa,
|
||||
protocol: Protocol::Udp,
|
||||
tls_dns_name: None,
|
||||
trust_negative_responses: false,
|
||||
bind_addr: None,
|
||||
});
|
||||
Ok(TokioAsyncResolver::tokio(cfg, opts))
|
||||
} else {
|
||||
Ok(TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), opts))
|
||||
}
|
||||
}
|
||||
|
||||
fn all_types() -> Vec<(&'static str, RecordType)> {
|
||||
vec![
|
||||
("A", RecordType::A),
|
||||
("AAAA", RecordType::AAAA),
|
||||
("MX", RecordType::MX),
|
||||
("NS", RecordType::NS),
|
||||
("CNAME", RecordType::CNAME),
|
||||
("TXT", RecordType::TXT),
|
||||
("SOA", RecordType::SOA),
|
||||
("CAA", RecordType::CAA),
|
||||
("SRV", RecordType::SRV),
|
||||
("PTR", RecordType::PTR),
|
||||
]
|
||||
}
|
||||
|
||||
async fn try_axfr(domain: &str, nameserver: &str) -> AxfrResult {
|
||||
// Best-effort AXFR via raw TCP to port 53.
|
||||
// Build a minimal DNS AXFR query (type 252) and read packets.
|
||||
// Most public servers will refuse (REFUSED rcode) — this is mostly a
|
||||
// "is this server misconfigured" check.
|
||||
let addr: SocketAddr = match format!("{nameserver}:53").parse() {
|
||||
Ok(a) => a,
|
||||
Err(e) => return AxfrResult { success: false, nameserver: nameserver.into(), records_dumped: vec![], error: Some(e.to_string()) },
|
||||
};
|
||||
|
||||
let stream_res = timeout(Duration::from_secs(5), TcpStream::connect(addr)).await;
|
||||
let mut stream = match stream_res {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return AxfrResult { success: false, nameserver: nameserver.into(), records_dumped: vec![], error: Some(e.to_string()) },
|
||||
Err(_) => return AxfrResult { success: false, nameserver: nameserver.into(), records_dumped: vec![], error: Some("connect timeout".into()) },
|
||||
};
|
||||
|
||||
let mut query: Vec<u8> = Vec::new();
|
||||
// DNS header (12 bytes): tx=0, flags=0x0100 (RD), questions=1
|
||||
query.extend_from_slice(&[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]);
|
||||
// QNAME
|
||||
for label in domain.trim_end_matches('.').split('.') {
|
||||
let bytes = label.as_bytes();
|
||||
query.push(bytes.len() as u8);
|
||||
query.extend_from_slice(bytes);
|
||||
}
|
||||
query.push(0);
|
||||
// QTYPE=252 AXFR, QCLASS=IN
|
||||
query.extend_from_slice(&[0, 252, 0, 1]);
|
||||
|
||||
// TCP DNS prepends 2-byte length
|
||||
let mut framed = Vec::with_capacity(query.len() + 2);
|
||||
let len = query.len() as u16;
|
||||
framed.extend_from_slice(&len.to_be_bytes());
|
||||
framed.extend_from_slice(&query);
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
if let Err(e) = stream.write_all(&framed).await {
|
||||
return AxfrResult { success: false, nameserver: nameserver.into(), records_dumped: vec![], error: Some(e.to_string()) };
|
||||
}
|
||||
let _ = stream.flush().await;
|
||||
|
||||
// Read up to 32KB of responses, parse roughly — just count record names.
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut total = 0usize;
|
||||
loop {
|
||||
let mut len_buf = [0u8; 2];
|
||||
let r = timeout(Duration::from_secs(4), stream.read_exact(&mut len_buf)).await;
|
||||
match r {
|
||||
Ok(Ok(_)) => {}
|
||||
_ => break,
|
||||
}
|
||||
let rlen = u16::from_be_bytes(len_buf) as usize;
|
||||
if rlen == 0 || rlen > 65535 { break; }
|
||||
let mut buf = vec![0u8; rlen];
|
||||
if timeout(Duration::from_secs(4), stream.read_exact(&mut buf)).await.is_err() { break; }
|
||||
total += buf.len();
|
||||
// check RCODE in response flags
|
||||
if buf.len() >= 4 {
|
||||
let rcode = buf[3] & 0x0F;
|
||||
if rcode != 0 {
|
||||
return AxfrResult {
|
||||
success: false,
|
||||
nameserver: nameserver.into(),
|
||||
records_dumped: vec![],
|
||||
error: Some(format!("RCODE {rcode} (refused/notauth/other)")),
|
||||
};
|
||||
}
|
||||
}
|
||||
// naive: parse names from answer section (skip header + question)
|
||||
if let Some(names) = extract_names(&buf) {
|
||||
for n in names {
|
||||
if !out.contains(&n) { out.push(n); }
|
||||
}
|
||||
}
|
||||
if total > 65536 { break; }
|
||||
}
|
||||
|
||||
AxfrResult {
|
||||
success: !out.is_empty(),
|
||||
nameserver: nameserver.into(),
|
||||
records_dumped: out,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort name extractor — walks bytes looking for printable labels.
|
||||
fn extract_names(packet: &[u8]) -> Option<Vec<String>> {
|
||||
if packet.len() < 12 { return None; }
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut i = 12; // skip header
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
while i < packet.len() {
|
||||
let len = packet[i] as usize;
|
||||
if len == 0 { i += 1; continue; }
|
||||
if len & 0xC0 == 0xC0 { i += 2; continue; }
|
||||
if i + 1 + len > packet.len() { break; }
|
||||
if len < 64 {
|
||||
let bytes = &packet[i + 1..i + 1 + len];
|
||||
if bytes.iter().all(|b| b.is_ascii_graphic()) {
|
||||
let s = String::from_utf8_lossy(bytes).to_string();
|
||||
if !seen.contains(&s) && s.len() > 1 {
|
||||
seen.insert(s.clone());
|
||||
out.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1 + len;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dns_query(req: DnsRequest) -> Result<DnsReport, String> {
|
||||
let resolver = build_resolver(req.resolver.as_deref())?;
|
||||
let wanted: Vec<(&'static str, RecordType)> = match &req.types {
|
||||
Some(v) if !v.is_empty() => all_types().into_iter().filter(|(n, _)| v.iter().any(|x| x.eq_ignore_ascii_case(n))).collect(),
|
||||
_ => all_types(),
|
||||
};
|
||||
|
||||
let mut records: Vec<DnsRecordGroup> = Vec::new();
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
let mut nameservers: Vec<String> = Vec::new();
|
||||
|
||||
for (name, rt) in wanted {
|
||||
let result = resolver.lookup(req.domain.as_str(), rt).await;
|
||||
match result {
|
||||
Ok(lookup) => {
|
||||
let values: Vec<String> = lookup.iter().map(|r| r.to_string()).collect();
|
||||
if !values.is_empty() {
|
||||
if name == "NS" {
|
||||
for v in &values { nameservers.push(v.trim_end_matches('.').to_string()); }
|
||||
}
|
||||
records.push(DnsRecordGroup { rtype: name.to_string(), values });
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("{name}: {e}");
|
||||
// "no records found" is noise — only log if not NoRecordsFound
|
||||
if !msg.to_lowercase().contains("no record") {
|
||||
errors.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DNSSEC check — presence of DNSKEY
|
||||
let dnssec = match resolver.lookup(req.domain.as_str(), RecordType::DNSKEY).await {
|
||||
Ok(lookup) => {
|
||||
let count = lookup.iter().count();
|
||||
DnssecCheck { dnskey_count: count, has_dnssec: count > 0 }
|
||||
}
|
||||
Err(_) => DnssecCheck { dnskey_count: 0, has_dnssec: false },
|
||||
};
|
||||
|
||||
// AXFR attempt
|
||||
let axfr = if req.try_axfr {
|
||||
let ns = nameservers.first().cloned().unwrap_or_default();
|
||||
if ns.is_empty() {
|
||||
Some(AxfrResult { success: false, nameserver: String::new(), records_dumped: vec![], error: Some("no NS records to attempt AXFR".into()) })
|
||||
} else {
|
||||
Some(try_axfr(&req.domain, &ns).await)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(DnsReport { domain: req.domain, records, axfr, dnssec, errors })
|
||||
}
|
||||