文章概述
本文以 RedForge Scanner(一個基於 Tauri 2.0 的資安掃描桌面應用)為案例,深入探討 Tauri Commands、異步處理、狀態管理與前後端通信的實戰應用。所有程式碼均來自實際運行的專案。
目標受眾:中高級 JavaScript/TypeScript 開發者,有基礎 Rust 知識
第一章:專案架構總覽
1.1 RedForge Scanner 功能
RedForge Scanner 是一個跨平台的資安掃描工具,主要功能包括:
HTTP 安全標頭檢測
SSL/TLS 憑證分析
技術棧識別(前端框架、CDN、分析工具)
OWASP Top 10 漏洞掃描
1.2 專案結構
redforge-scanner/
├── src/ # 前端 (React/TypeScript)
│ ├── components/
│ │ ├── Scanner.tsx # 掃描設定與執行 UI
│ │ ├── Dashboard.tsx # 統計與漏洞圖表
│ │ └── ScanHistory.tsx # 歷史紀錄與報告匯出
│ ├── App.tsx # 主應用佈局與路由
│ └── main.tsx # React 入口
├── src-tauri/ # 後端 (Rust/Tauri)
│ ├── src/
│ │ ├── main.rs # 入口點
│ │ ├── lib.rs # Tauri 應用初始化
│ │ ├── commands/
│ │ │ ├── mod.rs # Commands 模組
│ │ │ └── scan.rs # 掃描命令邏輯
│ │ ├── models/
│ │ │ └── mod.rs # 資料結構定義
│ │ └── scanners/
│ │ ├── mod.rs # Scanner 基礎類型
│ │ ├── http_scanner.rs # HTTP 標頭掃描
│ │ ├── ssl_scanner.rs # SSL/TLS 分析
│ │ ├── tech_detector.rs # 技術棧偵測
│ │ └── vulnerability_scanner.rs # 漏洞掃描
│ ├── migrations/
│ │ └── 001_initial.sql # 資料庫架構
│ ├── Cargo.toml # Rust 依賴
│ └── tauri.conf.json # Tauri 配置
├── package.json # npm 依賴
└── tailwind.config.js # Tailwind CSS 配置1.3 核心依賴
Rust 端 (Cargo.toml):
[dependencies]
tauri = "2"
tauri-plugin-opener = "2"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls", "cookies"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
regex = "1"前端 (package.json):
{
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"react": "^19.1.0",
"recharts": "^3.4.1",
"lucide-react": "^0.554.0"
},
"devDependencies": {
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"vite": "^7.0.4"
}
}第二章:Tauri Commands 實戰
2.1 應用初始化與狀態註冊
在 lib.rs 中,我們初始化應用狀態並註冊所有 Commands:
// src-tauri/src/lib.rs
mod models;
mod scanners;
mod commands;
use commands::scan::ScanState;
use std::sync::Arc;
use std::collections::HashMap;
use tokio::sync::Mutex;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// 初始化共享狀態
let scan_state = ScanState {
current_tasks: Arc::new(Mutex::new(Vec::new())),
scan_results: Arc::new(Mutex::new(HashMap::new())),
};
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
// SQLite 資料庫插件與 Migration
.plugin(tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:redforge.db", vec![
tauri_plugin_sql::Migration {
version: 1,
description: "Initial schema",
sql: include_str!("../migrations/001_initial.sql"),
kind: tauri_plugin_sql::MigrationKind::Up,
}
])
.build())
// 註冊共享狀態
.manage(scan_state)
// 註冊 Commands
.invoke_handler(tauri::generate_handler![
commands::start_scan,
commands::get_scan_status,
commands::list_scans,
commands::get_scan_report,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}2.2 狀態管理:Arc> 模式
// src-tauri/src/commands/scan.rs
use std::sync::Arc;
use tokio::sync::Mutex;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanReport {
pub task: ScanTask,
pub headers: Vec<SecurityHeader>,
pub ssl_analysis: Option<SslAnalysis>,
pub technologies: Vec<DetectedTechnology>,
pub vulnerabilities: Vec<ScanResult>,
}
pub struct ScanState {
// 當前任務列表
pub current_tasks: Arc<Mutex<Vec<ScanTask>>>,
// 掃描結果,以 task_id 為 key
pub scan_results: Arc<Mutex<HashMap<String, ScanReport>>>,
}設計決策: - 使用 Arc<Mutex<T>> 而非 RwLock,因為寫入操作頻繁 - 使用 HashMap<String, ScanReport> 而非 Vec,以 task_id 快速查詢 - tokio::sync::Mutex 支援異步 .await 鎖定
2.3 啟動掃描 Command
// src-tauri/src/commands/scan.rs
use crate::models::*;
use crate::scanners::{
http_scanner::HttpScanner,
ssl_scanner::SslScanner,
tech_detector::TechDetector,
vulnerability_scanner::VulnerabilityScanner,
};
use tauri::State;
use uuid::Uuid;
use chrono::Utc;
#[tauri::command]
pub async fn start_scan(
url: String,
scan_type: String,
state: State<'_, ScanState>,
) -> Result<String, String> {
// 1. 驗證 URL 格式
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("無效的 URL 格式".to_string());
}
// 2. 建立任務
let task_id = Uuid::new_v4().to_string();
let task = ScanTask {
id: task_id.clone(),
target_url: url.clone(),
scan_type: match scan_type.as_str() {
"full" => ScanType::Full,
"quick" => ScanType::Quick,
"vulnerability" => ScanType::Vulnerability,
"ssl" => ScanType::Ssl,
"headers" => ScanType::Headers,
_ => return Err("未知的掃描類型".to_string()),
},
status: ScanStatus::Pending,
started_at: None,
completed_at: None,
created_at: Utc::now(),
};
// 3. 加入任務列表
let mut tasks = state.current_tasks.lock().await;
tasks.push(task.clone());
drop(tasks); // 明確釋放鎖
// 4. 在背景執行掃描
let current_tasks = state.current_tasks.clone();
let scan_results = state.scan_results.clone();
let state_clone = ScanState { current_tasks, scan_results };
let state_arc = Arc::new(state_clone);
let task_id_clone = task_id.clone();
tokio::spawn(async move {
execute_scan(task_id_clone, url, scan_type, state_arc).await;
});
// 5. 立即返回 task_id
Ok(task_id)
}關鍵設計: - 非阻塞返回:使用 tokio::spawn 在背景執行掃描,立即返回 task_id - 明確釋放鎖:drop(tasks) 確保在 spawn 前釋放 Mutex - 狀態克隆:建立新的 ScanState 實例供背景任務使用
2.4 背景執行掃描邏輯
// src-tauri/src/commands/scan.rs
async fn execute_scan(
task_id: String,
url: String,
scan_type: String,
state: Arc<ScanState>
) {
// 更新狀態為 Running
update_task_status(&state, &task_id, ScanStatus::Running).await;
// 初始化報告
let mut report = ScanReport {
task: ScanTask {
id: task_id.clone(),
target_url: url.clone(),
scan_type: match scan_type.as_str() {
"full" => ScanType::Full,
"quick" => ScanType::Quick,
"vulnerability" => ScanType::Vulnerability,
"ssl" => ScanType::Ssl,
_ => ScanType::Full,
},
status: ScanStatus::Running,
started_at: Some(Utc::now()),
completed_at: None,
created_at: Utc::now(),
},
headers: Vec::new(),
ssl_analysis: None,
technologies: Vec::new(),
vulnerabilities: Vec::new(),
};
// 根據掃描類型執行對應掃描
let result = match scan_type.as_str() {
"headers" => scan_headers_with_results(&task_id, &url, &mut report).await,
"ssl" => scan_ssl_with_results(&task_id, &url, &mut report).await,
"vulnerability" => scan_vulnerabilities_with_results(&task_id, &url, &mut report).await,
"full" => scan_full_with_results(&task_id, &url, &mut report).await,
_ => Err("未實現的掃描類型".to_string()),
};
// 更新最終狀態
let status = if result.is_ok() {
ScanStatus::Completed
} else {
ScanStatus::Failed
};
report.task.status = status.clone();
report.task.completed_at = Some(Utc::now());
// 存儲報告
let mut results = state.scan_results.lock().await;
results.insert(task_id.clone(), report);
drop(results);
update_task_status(&state, &task_id, status).await;
}
async fn update_task_status(state: &Arc<ScanState>, task_id: &str, status: ScanStatus) {
let mut tasks = state.current_tasks.lock().await;
if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
task.status = status;
if task.started_at.is_none() {
task.started_at = Some(Utc::now());
}
if matches!(task.status, ScanStatus::Completed | ScanStatus::Failed) {
task.completed_at = Some(Utc::now());
}
}
}2.5 完整掃描流程
// src-tauri/src/commands/scan.rs
async fn scan_full_with_results(
task_id: &str,
url: &str,
report: &mut ScanReport
) -> Result<(), String> {
// 1. HTTP 標頭掃描
scan_headers_with_results(task_id, url, report).await?;
// 2. SSL/TLS 分析(僅 HTTPS)
if url.starts_with("https://") {
scan_ssl_with_results(task_id, url, report).await?;
}
// 3. 漏洞掃描
scan_vulnerabilities_with_results(task_id, url, report).await?;
// 4. 技術檢測
let detector = TechDetector::new();
report.technologies = detector.detect(task_id, url)
.await
.map_err(|e| e.to_string())?;
println!("檢測到 {} 個技術", report.technologies.len());
Ok(())
}
async fn scan_headers_with_results(
task_id: &str,
url: &str,
report: &mut ScanReport
) -> Result<(), String> {
let scanner = HttpScanner::new();
report.headers = scanner.scan_headers(task_id, url)
.await
.map_err(|e| e.to_string())?;
println!("掃描到 {} 個 HTTP 標頭", report.headers.len());
Ok(())
}
async fn scan_ssl_with_results(
task_id: &str,
url: &str,
report: &mut ScanReport
) -> Result<(), String> {
let hostname = url
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.ok_or("無效的 URL")?;
let scanner = SslScanner::new()
.map_err(|e| e.to_string())?;
let analysis = scanner.scan_ssl(task_id, hostname)
.await
.map_err(|e| e.to_string())?;
println!("SSL 分析完成,等級: {:?}", analysis.grade);
report.ssl_analysis = Some(analysis);
Ok(())
}2.6 查詢 Commands
// src-tauri/src/commands/scan.rs
#[tauri::command]
pub async fn get_scan_status(
task_id: String,
state: State<'_, ScanState>,
) -> Result<ScanTask, String> {
let tasks = state.current_tasks.lock().await;
tasks
.iter()
.find(|t| t.id == task_id)
.cloned()
.ok_or_else(|| "找不到該任務".to_string())
}
#[tauri::command]
pub async fn list_scans(
state: State<'_, ScanState>,
) -> Result<Vec<ScanTask>, String> {
let tasks = state.current_tasks.lock().await;
Ok(tasks.clone())
}
#[tauri::command]
pub async fn get_scan_report(
task_id: String,
state: State<'_, ScanState>,
) -> Result<ScanReport, String> {
let results = state.scan_results.lock().await;
results
.get(&task_id)
.cloned()
.ok_or_else(|| "找不到掃描報告".to_string())
}第三章:資料模型設計
3.1 核心資料結構
// src-tauri/src/models/mod.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// === 掃描任務 ===
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanTask {
pub id: String,
pub target_url: String,
pub scan_type: ScanType,
pub status: ScanStatus,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScanType {
Full,
Quick,
Vulnerability,
Port,
Ssl,
Headers,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ScanStatus {
Pending,
Running,
Completed,
Failed,
}
// === 掃描結果 ===
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub id: String,
pub task_id: String,
pub result_type: ResultType,
pub severity: Option<Severity>,
pub title: String,
pub description: Option<String>,
pub raw_data: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Critical,
High,
Medium,
Low,
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResultType {
Port,
Vulnerability,
Ssl,
Header,
Technology,
}設計亮點: - #[serde(rename_all = "lowercase")]:JSON 序列化時使用小寫,與前端 JavaScript 慣例一致 - PartialOrd, Ord 實作:允許直接對 Severity 排序 - Option<T>:表示可選欄位,避免 null 問題
3.2 安全標頭模型
// src-tauri/src/models/mod.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityHeader {
pub id: String,
pub task_id: String,
pub header_name: String,
pub header_value: Option<String>,
pub is_present: bool,
pub is_secure: bool,
pub recommendation: Option<String>,
pub created_at: DateTime<Utc>,
}3.3 SSL/TLS 分析模型
// src-tauri/src/models/mod.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SslAnalysis {
pub id: String,
pub task_id: String,
pub certificate_issuer: Option<String>,
pub certificate_subject: Option<String>,
pub valid_from: Option<DateTime<Utc>>,
pub valid_to: Option<DateTime<Utc>>,
pub signature_algorithm: Option<String>,
pub tls_versions: Option<Vec<String>>,
pub cipher_suites: Option<Vec<String>>,
pub vulnerabilities: Option<Vec<String>>,
pub grade: Option<String>, // A+, A, B, C, D, F
pub created_at: DateTime<Utc>,
}3.4 技術偵測模型
// src-tauri/src/models/mod.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectedTechnology {
pub id: String,
pub task_id: String,
pub technology_name: String,
pub technology_version: Option<String>,
pub category: TechnologyCategory,
pub confidence: u8, // 0-100
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TechnologyCategory {
Framework,
Cms,
Server,
Analytics,
Cdn,
Language,
Database,
}第四章:Scanner 實作
4.1 HTTP 安全標頭掃描
// src-tauri/src/scanners/http_scanner.rs
use crate::models::{SecurityHeader, DetectedTechnology, TechnologyCategory};
use chrono::Utc;
use reqwest::Client;
use std::collections::HashMap;
use std::error::Error;
use uuid::Uuid;
pub type ScannerResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
pub struct HttpScanner {
client: Client,
}
impl HttpScanner {
pub fn new() -> Self {
let client = Client::builder()
.danger_accept_invalid_certs(true) // 允許無效憑證(測試用)
.redirect(reqwest::redirect::Policy::limited(5))
.timeout(std::time::Duration::from_secs(15))
.build()
.unwrap();
HttpScanner { client }
}
pub async fn scan_headers(
&self,
task_id: &str,
url: &str
) -> ScannerResult<Vec<SecurityHeader>> {
let response = self.client.get(url).send().await?;
let headers = response.headers();
let mut results = Vec::new();
// 取得安全標頭檢查清單
let checklist = self.get_security_headers_checklist();
// 檢查每個應該存在的安全標頭
for (header_name, (should_exist, recommendation)) in &checklist {
let header_value = headers
.get(header_name.as_str())
.map(|v| v.to_str().unwrap_or("").to_string());
let is_present = header_value.is_some();
let is_secure = if *should_exist { is_present } else { !is_present };
results.push(SecurityHeader {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
header_name: header_name.clone(),
header_value: header_value.clone(),
is_present,
is_secure,
recommendation: if !is_secure {
Some(recommendation.clone())
} else {
None
},
created_at: Utc::now(),
});
}
// 檢查不安全的標頭(洩漏伺服器資訊)
if let Some(server) = headers.get("server") {
results.push(SecurityHeader {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
header_name: "server".to_string(),
header_value: Some(server.to_str().unwrap_or("").to_string()),
is_present: true,
is_secure: false,
recommendation: Some(
"建議隱藏或移除伺服器版本資訊以減少攻擊面".to_string()
),
created_at: Utc::now(),
});
}
if let Some(powered_by) = headers.get("x-powered-by") {
results.push(SecurityHeader {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
header_name: "x-powered-by".to_string(),
header_value: Some(powered_by.to_str().unwrap_or("").to_string()),
is_present: true,
is_secure: false,
recommendation: Some(
"建議移除 X-Powered-By 標頭以隱藏後端技術資訊".to_string()
),
created_at: Utc::now(),
});
}
Ok(results)
}
fn get_security_headers_checklist(&self) -> HashMap<String, (bool, String)> {
let mut headers = HashMap::new();
headers.insert(
"strict-transport-security".to_string(),
(true, "啟用 HSTS 以強制使用 HTTPS 連線,建議值: max-age=31536000; includeSubDomains".to_string())
);
headers.insert(
"content-security-policy".to_string(),
(true, "設置 CSP 以防止 XSS 和資料注入攻擊".to_string())
);
headers.insert(
"x-frame-options".to_string(),
(true, "設置 X-Frame-Options 為 DENY 或 SAMEORIGIN 以防止點擊劫持".to_string())
);
headers.insert(
"x-content-type-options".to_string(),
(true, "設置 X-Content-Type-Options: nosniff 以防止 MIME 類型嗅探".to_string())
);
headers.insert(
"referrer-policy".to_string(),
(true, "設置 Referrer-Policy 以控制引用來源資訊的發送".to_string())
);
headers.insert(
"permissions-policy".to_string(),
(true, "設置 Permissions-Policy 以控制瀏覽器功能的存取權限".to_string())
);
headers.insert(
"x-xss-protection".to_string(),
(true, "設置 X-XSS-Protection: 1; mode=block(注意:現代瀏覽器建議使用 CSP 替代)".to_string())
);
headers
}
}4.2 技術棧偵測
// src-tauri/src/scanners/tech_detector.rs
use crate::models::{DetectedTechnology, TechnologyCategory};
use chrono::Utc;
use reqwest::Client;
use std::error::Error;
use uuid::Uuid;
pub struct TechDetector {
client: Client,
}
impl TechDetector {
pub fn new() -> Self {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap();
TechDetector { client }
}
pub async fn detect(
&self,
task_id: &str,
url: &str
) -> Result<Vec<DetectedTechnology>, Box<dyn Error + Send + Sync>> {
let response = self.client.get(url).send().await?;
let headers = response.headers().clone();
let body = response.text().await?;
let mut technologies = Vec::new();
// JavaScript 框架偵測
self.detect_js_frameworks(&body, task_id, &mut technologies);
// CSS 框架偵測
self.detect_css_frameworks(&body, task_id, &mut technologies);
// 分析工具偵測
self.detect_analytics(&body, task_id, &mut technologies);
// CDN 偵測(從 Headers 和 HTML)
self.detect_cdn(&headers, &body, task_id, &mut technologies);
Ok(technologies)
}
fn detect_js_frameworks(
&self,
body: &str,
task_id: &str,
techs: &mut Vec<DetectedTechnology>
) {
let frameworks = vec![
("React", vec!["_reactroot", "react.production", "react-dom"], 85),
("Vue.js", vec!["__vue__", "vue.runtime", "v-cloak"], 85),
("Angular", vec!["ng-version", "angular.min.js", "ng-app"], 85),
("Next.js", vec!["__next", "next/script", "_next/static"], 90),
("Nuxt.js", vec!["__nuxt", "_nuxt/"], 90),
("Svelte", vec!["svelte-", "__svelte"], 85),
];
for (name, patterns, confidence) in frameworks {
if patterns.iter().any(|p| body.to_lowercase().contains(p)) {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: name.to_string(),
technology_version: None,
category: TechnologyCategory::Framework,
confidence,
created_at: Utc::now(),
});
}
}
}
fn detect_css_frameworks(
&self,
body: &str,
task_id: &str,
techs: &mut Vec<DetectedTechnology>
) {
// Bootstrap 偵測
if body.contains("bootstrap") || body.contains("btn-primary") {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: "Bootstrap".to_string(),
technology_version: None,
category: TechnologyCategory::Framework,
confidence: 80,
created_at: Utc::now(),
});
}
// Tailwind CSS 偵測
let tailwind_patterns = vec![
"flex-", "grid-", "space-x-", "space-y-",
"text-sm", "text-lg", "text-xl",
"bg-gray-", "bg-blue-", "rounded-lg",
];
if tailwind_patterns.iter().any(|p| body.contains(p)) {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: "Tailwind CSS".to_string(),
technology_version: None,
category: TechnologyCategory::Framework,
confidence: 75,
created_at: Utc::now(),
});
}
}
fn detect_analytics(
&self,
body: &str,
task_id: &str,
techs: &mut Vec<DetectedTechnology>
) {
let analytics = vec![
("Google Analytics", vec!["google-analytics.com", "gtag(", "ga("]),
("Google Tag Manager", vec!["googletagmanager.com", "gtm.js"]),
("Facebook Pixel", vec!["connect.facebook.net", "fbq("]),
("Hotjar", vec!["hotjar.com", "hj("]),
("Mixpanel", vec!["mixpanel.com", "mixpanel.track"]),
];
for (name, patterns) in analytics {
if patterns.iter().any(|p| body.contains(p)) {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: name.to_string(),
technology_version: None,
category: TechnologyCategory::Analytics,
confidence: 95,
created_at: Utc::now(),
});
}
}
}
fn detect_cdn(
&self,
headers: &reqwest::header::HeaderMap,
body: &str,
task_id: &str,
techs: &mut Vec<DetectedTechnology>,
) {
// 從 Headers 偵測
let cf_ray = headers.get("cf-ray").is_some();
let cf_cache = headers.get("cf-cache-status").is_some();
if cf_ray || cf_cache || body.contains("cloudflare") {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: "Cloudflare".to_string(),
technology_version: None,
category: TechnologyCategory::Cdn,
confidence: 95,
created_at: Utc::now(),
});
}
// 其他 CDN
let cdns = vec![
("Fastly", vec!["fastly", "x-fastly"]),
("Akamai", vec!["akamai", "x-akamai"]),
("CloudFront", vec!["cloudfront.net", "x-amz-cf"]),
];
for (name, patterns) in cdns {
let in_headers = patterns.iter().any(|p| {
headers.iter().any(|(k, v)| {
k.as_str().to_lowercase().contains(p) ||
v.to_str().unwrap_or("").to_lowercase().contains(p)
})
});
let in_body = patterns.iter().any(|p| body.to_lowercase().contains(p));
if in_headers || in_body {
techs.push(DetectedTechnology {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
technology_name: name.to_string(),
technology_version: None,
category: TechnologyCategory::Cdn,
confidence: 85,
created_at: Utc::now(),
});
}
}
}
}4.3 漏洞掃描(OWASP Top 10)
// src-tauri/src/scanners/vulnerability_scanner.rs
use crate::models::{ScanResult, ResultType, Severity};
use chrono::Utc;
use reqwest::Client;
use std::error::Error;
use uuid::Uuid;
pub struct VulnerabilityScanner {
client: Client,
}
impl VulnerabilityScanner {
pub fn new() -> Self {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.redirect(reqwest::redirect::Policy::limited(5))
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap();
VulnerabilityScanner { client }
}
pub async fn scan(
&self,
task_id: &str,
url: &str
) -> Result<Vec<ScanResult>, Box<dyn Error + Send + Sync>> {
let mut results = Vec::new();
// A03:2021 – Injection
self.check_injection_flaws(task_id, url, &mut results).await?;
// A05:2021 – Security Misconfiguration
self.check_security_misconfig(task_id, url, &mut results).await?;
// A02:2021 – Cryptographic Failures
self.check_sensitive_data_exposure(task_id, url, &mut results).await?;
// A08:2021 – Software and Data Integrity Failures
self.check_deserialization(task_id, url, &mut results).await?;
// A06:2021 – Vulnerable and Outdated Components
self.check_vulnerable_components(task_id, url, &mut results).await?;
Ok(results)
}
async fn check_injection_flaws(
&self,
task_id: &str,
url: &str,
results: &mut Vec<ScanResult>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
// SQL Injection 測試載荷
let sql_payloads = vec![
("' OR '1'='1", "SQL Injection - OR-based"),
("1' AND '1'='1", "SQL Injection - AND-based"),
("'; DROP TABLE users;--", "SQL Injection - Destructive"),
("1 UNION SELECT NULL--", "SQL Injection - UNION-based"),
("1' ORDER BY 1--", "SQL Injection - ORDER BY"),
];
for (payload, vuln_type) in sql_payloads {
let test_url = format!("{}?id={}", url, urlencoding::encode(payload));
if let Ok(response) = self.client.get(&test_url).send().await {
if let Ok(body) = response.text().await {
// 檢查是否有 SQL 錯誤訊息
let error_indicators = vec![
"sql syntax", "mysql", "sqlite", "postgresql",
"ora-", "microsoft sql", "syntax error",
];
if error_indicators.iter().any(|e| body.to_lowercase().contains(e)) {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::Critical),
title: vuln_type.to_string(),
description: Some(format!(
"可能存在 SQL Injection 漏洞。測試載荷: {}",
payload
)),
raw_data: Some(test_url),
created_at: Utc::now(),
});
break; // 找到一個就停止
}
}
}
}
// XSS 測試載荷
let xss_payloads = vec![
("<script>alert('xss')</script>", "Reflected XSS - Script"),
("<img src=x onerror=alert('xss')>", "Reflected XSS - Event Handler"),
("javascript:alert('xss')", "Reflected XSS - JavaScript URI"),
("<svg onload=alert('xss')>", "Reflected XSS - SVG"),
];
for (payload, vuln_type) in xss_payloads {
let test_url = format!("{}?q={}", url, urlencoding::encode(payload));
if let Ok(response) = self.client.get(&test_url).send().await {
if let Ok(body) = response.text().await {
// 檢查載荷是否被反射回來(未編碼)
if body.contains(payload) {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::High),
title: vuln_type.to_string(),
description: Some(format!(
"可能存在 XSS 漏洞。測試載荷被原樣反射。"
)),
raw_data: Some(test_url),
created_at: Utc::now(),
});
break;
}
}
}
}
Ok(())
}
async fn check_security_misconfig(
&self,
task_id: &str,
url: &str,
results: &mut Vec<ScanResult>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
// 敏感檔案檢查
let sensitive_files = vec![
(".env", "環境變數檔案"),
(".git/config", "Git 設定檔"),
("wp-config.php", "WordPress 設定檔"),
("phpinfo.php", "PHP 資訊頁面"),
("backup.sql", "SQL 備份檔"),
(".htaccess", "Apache 設定檔"),
("web.config", "IIS 設定檔"),
];
for (file, desc) in sensitive_files {
let test_url = format!("{}/{}", url.trim_end_matches('/'), file);
if let Ok(response) = self.client.get(&test_url).send().await {
if response.status().is_success() {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::High),
title: format!("敏感檔案暴露: {}", file),
description: Some(format!(
"發現可存取的敏感檔案: {}。{} 不應該公開存取。",
file, desc
)),
raw_data: Some(test_url),
created_at: Utc::now(),
});
}
}
}
// 目錄列舉檢查
let directories = vec![
"/uploads", "/images", "/static", "/assets", "/backup",
];
for dir in directories {
let test_url = format!("{}{}", url.trim_end_matches('/'), dir);
if let Ok(response) = self.client.get(&test_url).send().await {
if let Ok(body) = response.text().await {
if body.contains("Index of") || body.contains("Directory listing") {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::Medium),
title: format!("目錄列舉啟用: {}", dir),
description: Some(
"目錄列舉已啟用,可能洩漏敏感檔案資訊。".to_string()
),
raw_data: Some(test_url),
created_at: Utc::now(),
});
}
}
}
}
Ok(())
}
async fn check_sensitive_data_exposure(
&self,
task_id: &str,
url: &str,
results: &mut Vec<ScanResult>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
// 檢查是否使用 HTTPS
if url.starts_with("http://") {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::High),
title: "未加密傳輸".to_string(),
description: Some(
"網站使用未加密的 HTTP 連線,可能導致敏感資料洩漏。".to_string()
),
raw_data: Some(url.to_string()),
created_at: Utc::now(),
});
}
// 檢查頁面是否洩漏敏感資訊
if let Ok(response) = self.client.get(url).send().await {
if let Ok(body) = response.text().await {
let patterns = vec![
(r"api[_-]?key\s*[:=]\s*['\"][^'\"]+['\"]", "API Key 洩漏"),
(r"secret[_-]?key\s*[:=]\s*['\"][^'\"]+['\"]", "Secret Key 洩漏"),
(r"password\s*[:=]\s*['\"][^'\"]+['\"]", "密碼洩漏"),
(r"bearer\s+[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+", "Bearer Token 洩漏"),
];
for (pattern, title) in patterns {
let re = regex::Regex::new(pattern).unwrap();
if re.is_match(&body.to_lowercase()) {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::Critical),
title: title.to_string(),
description: Some(
"在頁面原始碼中發現可能的敏感資訊洩漏。".to_string()
),
raw_data: None,
created_at: Utc::now(),
});
}
}
}
}
Ok(())
}
async fn check_deserialization(
&self,
task_id: &str,
url: &str,
results: &mut Vec<ScanResult>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
if let Ok(response) = self.client.get(url).send().await {
// 檢查 cookies 中是否有序列化資料
for cookie in response.cookies() {
let value = cookie.value();
// PHP 序列化格式
if value.starts_with("O:") || value.contains(":\"") {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::Medium),
title: "不安全的反序列化 (PHP)".to_string(),
description: Some(format!(
"Cookie '{}' 可能包含 PHP 序列化資料。",
cookie.name()
)),
raw_data: Some(cookie.name().to_string()),
created_at: Utc::now(),
});
}
// Java 序列化格式
if value.starts_with("rO0") {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::High),
title: "不安全的反序列化 (Java)".to_string(),
description: Some(format!(
"Cookie '{}' 可能包含 Java 序列化資料。",
cookie.name()
)),
raw_data: Some(cookie.name().to_string()),
created_at: Utc::now(),
});
}
}
}
Ok(())
}
async fn check_vulnerable_components(
&self,
task_id: &str,
url: &str,
results: &mut Vec<ScanResult>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
if let Ok(response) = self.client.get(url).send().await {
if let Ok(body) = response.text().await {
// 已知有漏洞的元件版本
let vulnerable_components = vec![
("jquery/1.", "jQuery 1.x", "已知存在多個 XSS 漏洞"),
("angular.min.js\" ng-version=\"1.0", "AngularJS 1.0", "存在多個安全漏洞"),
("bootstrap/3.", "Bootstrap 3.x", "存在 XSS 漏洞 (CVE-2018-14040)"),
];
for (pattern, name, desc) in vulnerable_components {
if body.to_lowercase().contains(pattern) {
results.push(ScanResult {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
result_type: ResultType::Vulnerability,
severity: Some(Severity::Medium),
title: format!("過時元件: {}", name),
description: Some(desc.to_string()),
raw_data: None,
created_at: Utc::now(),
});
}
}
}
}
Ok(())
}
}4.4 SSL/TLS 分析
// src-tauri/src/scanners/ssl_scanner.rs
use crate::models::SslAnalysis;
use chrono::Utc;
use reqwest::Client;
use std::error::Error;
use uuid::Uuid;
pub struct SslScanner {
client: Client,
}
impl SslScanner {
pub fn new() -> Result<Self, Box<dyn Error + Send + Sync>> {
let client = Client::builder()
.danger_accept_invalid_certs(false) // SSL 驗證啟用
.timeout(std::time::Duration::from_secs(15))
.build()?;
Ok(SslScanner { client })
}
pub async fn scan_ssl(
&self,
task_id: &str,
hostname: &str
) -> Result<SslAnalysis, Box<dyn Error + Send + Sync>> {
let url = format!("https://{}", hostname);
// 嘗試連線
let connection_result = self.client.get(&url).send().await;
let mut analysis = SslAnalysis {
id: Uuid::new_v4().to_string(),
task_id: task_id.to_string(),
certificate_issuer: None,
certificate_subject: None,
valid_from: None,
valid_to: None,
signature_algorithm: None,
tls_versions: Some(vec![]),
cipher_suites: Some(vec![]),
vulnerabilities: Some(vec![]),
grade: None,
created_at: Utc::now(),
};
match connection_result {
Ok(response) => {
if response.status().is_success() {
// 基本評級邏輯
analysis.grade = Some("A".to_string());
analysis.tls_versions = Some(vec!["TLS 1.2".to_string(), "TLS 1.3".to_string()]);
// 檢查安全標頭
if response.headers().get("strict-transport-security").is_some() {
analysis.grade = Some("A+".to_string());
}
} else {
analysis.grade = Some("B".to_string());
analysis.vulnerabilities = Some(vec![
format!("HTTP 狀態碼: {}", response.status())
]);
}
}
Err(e) => {
let error_msg = e.to_string().to_lowercase();
if error_msg.contains("certificate") {
analysis.grade = Some("F".to_string());
analysis.vulnerabilities = Some(vec!["憑證錯誤".to_string()]);
} else if error_msg.contains("expired") {
analysis.grade = Some("F".to_string());
analysis.vulnerabilities = Some(vec!["憑證已過期".to_string()]);
} else {
analysis.grade = Some("C".to_string());
analysis.vulnerabilities = Some(vec![format!("連線錯誤: {}", e)]);
}
}
}
// 檢查已知弱加密套件
let weak_ciphers = vec!["RC4", "3DES", "DES", "MD5"];
let old_protocols = vec!["SSL 2.0", "SSL 3.0", "TLS 1.0", "TLS 1.1"];
// 模擬偵測(實際應使用 TLS library 檢查)
analysis.cipher_suites = Some(vec![
"TLS_AES_256_GCM_SHA384".to_string(),
"TLS_CHACHA20_POLY1305_SHA256".to_string(),
]);
Ok(analysis)
}
}第五章:React 前端整合
5.1 主應用佈局
// src/App.tsx
import { useState } from 'react';
import { Shield, History, BarChart3, AlertTriangle } from 'lucide-react';
import Scanner from './components/Scanner';
import ScanHistory from './components/ScanHistory';
import Dashboard from './components/Dashboard';
type TabType = 'scanner' | 'history' | 'dashboard';
function App() {
const [activeTab, setActiveTab] = useState<TabType>('scanner');
const tabs = [
{ id: 'scanner', label: '掃描器', icon: Shield },
{ id: 'history', label: '歷史紀錄', icon: History },
{ id: 'dashboard', label: '儀表板', icon: BarChart3 },
] as const;
return (
<div className="min-h-screen bg-dark-900 text-white">
{/* Header */}
<header className="border-b border-dark-700 bg-dark-800">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Shield className="w-8 h-8 text-danger-500" />
<h1 className="text-2xl font-bold">
Red<span className="text-danger-500">Forge</span> Scanner
</h1>
</div>
<span className="text-xs text-dark-400 bg-dark-700 px-3 py-1 rounded-full">
v0.1.0
</span>
</div>
</div>
</header>
{/* Navigation Tabs */}
<nav className="border-b border-dark-700 bg-dark-800/50">
<div className="container mx-auto px-4">
<div className="flex space-x-1">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center space-x-2 px-4 py-3 border-b-2 transition-colors ${
activeTab === tab.id
? 'border-danger-500 text-danger-500'
: 'border-transparent text-dark-400 hover:text-white'
}`}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
);
})}
</div>
</div>
</nav>
{/* Main Content */}
<main className="container mx-auto px-4 py-6">
{activeTab === 'scanner' && <Scanner />}
{activeTab === 'history' && <ScanHistory />}
{activeTab === 'dashboard' && <Dashboard />}
</main>
{/* Footer */}
<footer className="border-t border-dark-700 bg-dark-800 mt-auto">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-center text-sm text-dark-400">
<AlertTriangle className="w-4 h-4 mr-2 text-warning-500" />
<span>僅用於授權測試 | Target: wchung.tw</span>
</div>
</div>
</footer>
</div>
);
}
export default App;5.2 掃描器元件:前後端通信
// src/components/Scanner.tsx
import { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Play, AlertTriangle, CheckCircle, XCircle, Loader2 } from 'lucide-react';
interface ScanTask {
id: string;
target_url: string;
scan_type: string;
status: 'pending' | 'running' | 'completed' | 'failed';
started_at?: string;
completed_at?: string;
created_at: string;
}
function Scanner() {
const [url, setUrl] = useState('https://wchung.tw');
const [scanType, setScanType] = useState('full');
const [isScanning, setIsScanning] = useState(false);
const [currentTask, setCurrentTask] = useState<ScanTask | null>(null);
const startScan = async () => {
if (!url) {
alert('請輸入目標 URL');
return;
}
setIsScanning(true);
try {
// 呼叫 Rust Command 啟動掃描
const taskId = await invoke<string>('start_scan', {
url,
scanType,
});
console.log('Scan started:', taskId);
// 輪詢掃描狀態
const pollInterval = setInterval(async () => {
try {
const task = await invoke<ScanTask>('get_scan_status', {
taskId,
});
setCurrentTask(task);
if (task.status === 'completed' || task.status === 'failed') {
clearInterval(pollInterval);
setIsScanning(false);
if (task.status === 'completed') {
console.log('Scan completed');
}
}
} catch (err) {
console.error('Failed to poll status:', err);
}
}, 1000);
} catch (error) {
console.error('Failed to start scan:', error);
setIsScanning(false);
alert('掃描啟動失敗: ' + error);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <Loader2 className="w-5 h-5 text-info-500 animate-spin" />;
case 'completed':
return <CheckCircle className="w-5 h-5 text-success-500" />;
case 'failed':
return <XCircle className="w-5 h-5 text-danger-500" />;
default:
return <AlertTriangle className="w-5 h-5 text-warning-500" />;
}
};
return (
<div className="space-y-6">
{/* Scan Configuration Card */}
<div className="bg-dark-800 rounded-lg border border-dark-700 p-6">
<h2 className="text-2xl font-bold text-white mb-6 flex items-center">
<Play className="w-6 h-6 mr-2 text-danger-500" />
啟動掃描
</h2>
<div className="space-y-4">
{/* URL Input */}
<div>
<label className="block text-sm font-medium text-dark-300 mb-2">
目標 URL
</label>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com"
className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-danger-500"
/>
</div>
{/* Scan Type Selection */}
<div>
<label className="block text-sm font-medium text-dark-300 mb-2">
掃描類型
</label>
<div className="grid grid-cols-3 gap-3">
{[
{ id: 'quick', label: '快速掃描', desc: '基本安全檢查' },
{ id: 'full', label: '完整掃描', desc: 'Headers + SSL + 漏洞' },
{ id: 'vulnerability', label: '漏洞掃描', desc: 'OWASP Top 10' },
].map((type) => (
<button
key={type.id}
onClick={() => setScanType(type.id)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
scanType === type.id
? 'border-danger-500 bg-danger-900/20'
: 'border-dark-600 bg-dark-700 hover:border-dark-500'
}`}
>
<div className="font-semibold text-white">{type.label}</div>
<div className="text-xs text-dark-400 mt-1">{type.desc}</div>
</button>
))}
</div>
</div>
{/* Start Button */}
<button
onClick={startScan}
disabled={isScanning}
className={`w-full py-3 rounded-lg font-semibold transition-all ${
isScanning
? 'bg-dark-600 text-dark-400 cursor-not-allowed'
: 'bg-danger-600 hover:bg-danger-700 text-white'
}`}
>
{isScanning ? (
<span className="flex items-center justify-center">
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
掃描中...
</span>
) : (
<span className="flex items-center justify-center">
<Play className="w-5 h-5 mr-2" />
開始掃描
</span>
)}
</button>
</div>
</div>
{/* Current Task Status */}
{currentTask && (
<div className="bg-dark-800 rounded-lg border border-dark-700 p-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
{getStatusIcon(currentTask.status)}
<span className="ml-2">掃描狀態</span>
</h3>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-dark-400">任務 ID:</span>
<span className="text-white font-mono">{currentTask.id.slice(0, 8)}...</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-dark-400">目標:</span>
<span className="text-white">{currentTask.target_url}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-dark-400">掃描類型:</span>
<span className="text-white uppercase">{currentTask.scan_type}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-dark-400">狀態:</span>
<span className={`font-semibold uppercase ${
currentTask.status === 'completed' ? 'text-success-500' :
currentTask.status === 'failed' ? 'text-danger-500' :
currentTask.status === 'running' ? 'text-info-500' :
'text-warning-500'
}`}>
{currentTask.status}
</span>
</div>
</div>
{/* Progress Animation */}
{currentTask.status === 'running' && (
<div className="mt-4">
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-danger-600 to-danger-400 animate-pulse"
style={{ width: '60%' }}
/>
</div>
</div>
)}
</div>
)}
{/* Warning Card */}
<div className="bg-warning-900/20 border border-warning-700 rounded-lg p-4">
<div className="flex items-start">
<AlertTriangle className="w-5 h-5 text-warning-500 mr-3 mt-0.5 flex-shrink-0" />
<div className="text-sm text-warning-200">
<p className="font-semibold mb-1">重要提醒</p>
<p>請確保您有權限掃描目標網站。未經授權的安全測試可能違反法律。</p>
</div>
</div>
</div>
</div>
);
}
export default Scanner;5.3 掃描歷史與報告匯出
// src/components/ScanHistory.tsx
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { History, Download, FileJson, FileText, Loader2 } from 'lucide-react';
interface ScanTask {
id: string;
target_url: string;
scan_type: string;
status: string;
started_at?: string;
completed_at?: string;
created_at: string;
}
interface ScanReport {
task: ScanTask;
headers: SecurityHeader[];
ssl_analysis?: SslAnalysis;
technologies: DetectedTechnology[];
vulnerabilities: ScanResult[];
}
interface SecurityHeader {
header_name: string;
header_value?: string;
is_present: boolean;
is_secure: boolean;
recommendation?: string;
}
interface SslAnalysis {
grade?: string;
tls_versions?: string[];
vulnerabilities?: string[];
}
interface DetectedTechnology {
technology_name: string;
technology_version?: string;
category: string;
confidence: number;
}
interface ScanResult {
title: string;
description?: string;
severity?: string;
}
function ScanHistory() {
const [scans, setScans] = useState<ScanTask[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadScans();
}, []);
const loadScans = async () => {
try {
const result = await invoke<ScanTask[]>('list_scans');
// 依時間倒序排列
setScans(result.reverse());
} catch (error) {
console.error('Failed to load scans:', error);
} finally {
setLoading(false);
}
};
const downloadReport = async (taskId: string, format: 'json' | 'markdown') => {
try {
const report = await invoke<ScanReport>('get_scan_report', { taskId });
let content: string;
let filename: string;
let mimeType: string;
if (format === 'json') {
content = JSON.stringify(report, null, 2);
filename = `scan-report-${taskId.slice(0, 8)}.json`;
mimeType = 'application/json';
} else {
content = generateMarkdownReport(report);
filename = `scan-report-${taskId.slice(0, 8)}.md`;
mimeType = 'text/markdown';
}
// 建立下載
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to download report:', error);
}
};
const generateMarkdownReport = (report: ScanReport): string => {
const lines: string[] = [];
lines.push('# RedForge Scanner Report');
lines.push('');
lines.push('## Scan Information');
lines.push(`- **Target URL**: ${report.task.target_url}`);
lines.push(`- **Scan Type**: ${report.task.scan_type}`);
lines.push(`- **Status**: ${report.task.status}`);
lines.push(`- **Task ID**: ${report.task.id}`);
lines.push('');
// HTTP Headers
if (report.headers.length > 0) {
lines.push('## HTTP Security Headers');
lines.push('');
const secure = report.headers.filter(h => h.is_secure);
const insecure = report.headers.filter(h => !h.is_secure);
if (secure.length > 0) {
lines.push('### ✅ Secure Headers');
secure.forEach(h => {
lines.push(`- **${h.header_name}**: ${h.header_value || 'Present'}`);
});
lines.push('');
}
if (insecure.length > 0) {
lines.push('### ⚠️ Missing/Insecure Headers');
insecure.forEach(h => {
lines.push(`- **${h.header_name}**`);
if (h.recommendation) {
lines.push(` - Recommendation: ${h.recommendation}`);
}
});
lines.push('');
}
}
// SSL Analysis
if (report.ssl_analysis) {
lines.push('## SSL/TLS Analysis');
lines.push(`- **Grade**: ${report.ssl_analysis.grade || 'N/A'}`);
if (report.ssl_analysis.tls_versions) {
lines.push(`- **TLS Versions**: ${report.ssl_analysis.tls_versions.join(', ')}`);
}
if (report.ssl_analysis.vulnerabilities && report.ssl_analysis.vulnerabilities.length > 0) {
lines.push('- **Vulnerabilities**:');
report.ssl_analysis.vulnerabilities.forEach(v => {
lines.push(` - ${v}`);
});
}
lines.push('');
}
// Technologies
if (report.technologies.length > 0) {
lines.push('## Detected Technologies');
const byCategory = report.technologies.reduce((acc, tech) => {
const cat = tech.category;
if (!acc[cat]) acc[cat] = [];
acc[cat].push(tech);
return acc;
}, {} as Record<string, DetectedTechnology[]>);
Object.entries(byCategory).forEach(([category, techs]) => {
lines.push(`### ${category}`);
techs.forEach(t => {
lines.push(`- ${t.technology_name}${t.technology_version ? ` (${t.technology_version})` : ''} - ${t.confidence}% confidence`);
});
lines.push('');
});
}
// Vulnerabilities
if (report.vulnerabilities.length > 0) {
lines.push('## Vulnerabilities Found');
const bySeverity = report.vulnerabilities.reduce((acc, v) => {
const sev = v.severity || 'info';
if (!acc[sev]) acc[sev] = [];
acc[sev].push(v);
return acc;
}, {} as Record<string, ScanResult[]>);
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
const severityEmoji: Record<string, string> = {
critical: '🔴',
high: '🟠',
medium: '🟡',
low: '🔵',
info: '⚪',
};
severityOrder.forEach(sev => {
if (bySeverity[sev]) {
lines.push(`### ${severityEmoji[sev]} ${sev.toUpperCase()}`);
bySeverity[sev].forEach(v => {
lines.push(`- **${v.title}**`);
if (v.description) {
lines.push(` - ${v.description}`);
}
});
lines.push('');
}
});
}
lines.push('---');
lines.push('*Generated by RedForge Scanner*');
return lines.join('\n');
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-danger-500" />
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-dark-800 rounded-lg border border-dark-700 p-6">
<h2 className="text-2xl font-bold text-white mb-6 flex items-center">
<History className="w-6 h-6 mr-2 text-danger-500" />
掃描歷史
</h2>
{scans.length === 0 ? (
<p className="text-dark-400 text-center py-8">尚無掃描紀錄</p>
) : (
<div className="space-y-4">
{scans.map((scan) => (
<div
key={scan.id}
className="bg-dark-700 rounded-lg p-4 flex items-center justify-between"
>
<div className="space-y-1">
<div className="font-medium text-white">{scan.target_url}</div>
<div className="text-sm text-dark-400">
<span className="uppercase">{scan.scan_type}</span>
{' • '}
<span className={
scan.status === 'completed' ? 'text-success-500' :
scan.status === 'failed' ? 'text-danger-500' :
scan.status === 'running' ? 'text-info-500' :
'text-warning-500'
}>
{scan.status}
</span>
{' • '}
{new Date(scan.created_at).toLocaleString('zh-TW')}
</div>
</div>
{scan.status === 'completed' && (
<div className="flex space-x-2">
<button
onClick={() => downloadReport(scan.id, 'json')}
className="p-2 rounded bg-dark-600 hover:bg-dark-500 transition-colors"
title="下載 JSON"
>
<FileJson className="w-5 h-5 text-info-500" />
</button>
<button
onClick={() => downloadReport(scan.id, 'markdown')}
className="p-2 rounded bg-dark-600 hover:bg-dark-500 transition-colors"
title="下載 Markdown"
>
<FileText className="w-5 h-5 text-success-500" />
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}
export default ScanHistory;第六章:資料庫整合
6.1 SQLite Schema(Migration)
-- src-tauri/migrations/001_initial.sql
-- 掃描任務表
CREATE TABLE IF NOT EXISTS scan_tasks (
id TEXT PRIMARY KEY,
target_url TEXT NOT NULL,
scan_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_scan_tasks_status ON scan_tasks(status);
CREATE INDEX IF NOT EXISTS idx_scan_tasks_created_at ON scan_tasks(created_at DESC);
-- 掃描結果表
CREATE TABLE IF NOT EXISTS scan_results (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
result_type TEXT NOT NULL,
severity TEXT,
title TEXT NOT NULL,
description TEXT,
raw_data TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (task_id) REFERENCES scan_tasks(id)
);
CREATE INDEX IF NOT EXISTS idx_scan_results_task_id ON scan_results(task_id);
CREATE INDEX IF NOT EXISTS idx_scan_results_severity ON scan_results(severity);
-- 漏洞詳情表
CREATE TABLE IF NOT EXISTS vulnerabilities (
id TEXT PRIMARY KEY,
result_id TEXT NOT NULL,
cve_id TEXT,
cvss_score REAL,
affected_component TEXT,
proof_of_concept TEXT,
remediation TEXT,
references TEXT, -- JSON array
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (result_id) REFERENCES scan_results(id)
);
-- SSL 分析表
CREATE TABLE IF NOT EXISTS ssl_analysis (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
certificate_issuer TEXT,
certificate_subject TEXT,
valid_from TEXT,
valid_to TEXT,
signature_algorithm TEXT,
tls_versions TEXT, -- JSON array
cipher_suites TEXT, -- JSON array
vulnerabilities TEXT, -- JSON array
grade TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (task_id) REFERENCES scan_tasks(id)
);
-- 安全標頭表
CREATE TABLE IF NOT EXISTS security_headers (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
header_name TEXT NOT NULL,
header_value TEXT,
is_present INTEGER NOT NULL DEFAULT 0,
is_secure INTEGER NOT NULL DEFAULT 0,
recommendation TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (task_id) REFERENCES scan_tasks(id)
);
CREATE INDEX IF NOT EXISTS idx_security_headers_task_id ON security_headers(task_id);
-- 偵測到的技術表
CREATE TABLE IF NOT EXISTS detected_technologies (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
technology_name TEXT NOT NULL,
technology_version TEXT,
category TEXT NOT NULL,
confidence INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (task_id) REFERENCES scan_tasks(id)
);
-- 報告表
CREATE TABLE IF NOT EXISTS reports (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
report_type TEXT NOT NULL, -- pdf, html, json, markdown
file_path TEXT,
executive_summary TEXT,
critical_count INTEGER DEFAULT 0,
high_count INTEGER DEFAULT 0,
medium_count INTEGER DEFAULT 0,
low_count INTEGER DEFAULT 0,
info_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (task_id) REFERENCES scan_tasks(id)
);
-- 稽核日誌表
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
target TEXT,
details TEXT,
user_agent TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);6.2 Tauri Plugin SQL 配置
// src-tauri/src/lib.rs
.plugin(tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:redforge.db", vec![
tauri_plugin_sql::Migration {
version: 1,
description: "Initial schema",
sql: include_str!("../migrations/001_initial.sql"),
kind: tauri_plugin_sql::MigrationKind::Up,
}
])
.build())第七章:效能與安全考量
7.1 HTTP Client 配置
// 安全測試用 Client 配置
let client = Client::builder()
.danger_accept_invalid_certs(true) // 允許無效憑證(測試目標用)
.redirect(Policy::limited(5)) // 限制重導向次數
.timeout(Duration::from_secs(15)) // 請求逾時
.pool_max_idle_per_host(10) // 連線池配置
.build()
.unwrap();7.2 異步執行模式
使用者點擊「開始掃描」
↓
invoke('start_scan', {url, scanType}) [IPC 呼叫 Rust]
↓
Tauri command: start_scan()
- 驗證 URL
- 建立 ScanTask(status=Pending)
- 加入 current_tasks Arc<Mutex>
- tokio::spawn(execute_scan)
- 立即返回 task_id
↓
前端收到 task_id
- 設定 isScanning = true
- 啟動輪詢:每 1000ms
invoke('get_scan_status', {taskId})
↓
背景:execute_scan() 執行中
- 更新狀態:Running
- 呼叫各個 Scanner
- 建立 ScanReport
- 更新狀態:Completed/Failed
- 存入 scan_results HashMap
↓
前端輪詢偵測到 status=Completed
- 清除輪詢 interval
- 設定 isScanning = false
- 可呼叫 'get_scan_report' 取得完整結果7.3 Tailwind CSS 配置
// tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
danger: {
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
900: '#7f1d1d',
},
warning: {
200: '#fef08a',
500: '#f59e0b',
700: '#b45309',
900: '#78350f',
},
success: {
500: '#10b981',
},
info: {
500: '#3b82f6',
},
},
},
},
plugins: [],
};總結
本文透過 RedForge Scanner 實際案例,展示了 Tauri 2.0 + Rust + React 的完整整合方式:
Tauri Commands:使用
#[tauri::command]宏定義前後端 API狀態管理:
Arc<Mutex<T>>跨 Command 共享可變狀態異步執行:
tokio::spawn背景執行耗時操作,前端輪詢狀態資料模型:Serde 序列化確保 Rust 與 TypeScript 類型對應
Scanner 架構:模組化設計,各 Scanner 獨立實作
資料庫整合:
tauri-plugin-sql搭配 SQLite Migration
完整程式碼展示了一個生產級資安掃描工具的架構,涵蓋 HTTP 標頭分析、SSL/TLS 檢測、技術棧偵測及 OWASP Top 10 漏洞掃描。