This commit is contained in:
taqin
2026-04-19 21:10:40 +07:00
parent 5fdd214fdc
commit 27381d4e37
211 changed files with 53571 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
docs/screenshots/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
imtaqin.id.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

17
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/imtaqin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

6
public/tauri.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

1
public/vite.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
screenshot/auto-pwn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
screenshot/dir-fuzz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
screenshot/form-brute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
screenshot/http-probe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
screenshot/http-prove Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
screenshot/jwt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
screenshot/jwtg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
screenshot/port-scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
screenshot/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

BIN
screenshot/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
screenshot/sqli-scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

BIN
screenshot/subdo-scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
screenshot/xploiter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
screenshot/xss-scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

7
src-tauri/.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

48
src-tauri/Cargo.toml Normal file
View 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
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View 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"
]
}

View 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
View 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
View 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

View 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")

View 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

View 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>

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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>

View 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>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">Pocket Pentester</string>
<string name="main_activity_title">Pocket Pentester</string>
</resources>

View 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>

View File

@@ -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>

View 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")
}

View 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")
}

View File

@@ -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()
}
}

View File

@@ -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
)
}
}
}
}
}

View 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

Binary file not shown.

View 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
View 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
View 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

View File

@@ -0,0 +1,3 @@
include ':app'
apply from: 'tauri.settings.gradle'

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

59
src-tauri/src/lib.rs Normal file
View 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
View 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()
}

View 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",
];

View 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,
})
}

View 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)
}

View 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()
}

View 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 })
}

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