From ca5702b3ceb1a2edcad1c5cf033a421f7c273e80 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:53:11 -0700 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20SOC=20lab=20content=20buildout=20?= =?UTF-8?q?=E2=80=94=20playbooks,=20detection,=20hardening,=20automation,?= =?UTF-8?q?=20monitoring=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete security operations reference library: - Playbooks: incident triage, phishing response, malware containment, unauthorized access — all MITRE ATT&CK mapped - Detection: 4 Sigma rules (brute force, PowerShell, SMB lateral movement, privilege escalation) + correlation patterns and threshold/anomaly guide - Hardening: SSH and Linux baselines, Ansible roles (common, ssh-hardening, firewall) with CIS-inspired defaults - Automation: auth log parser, backup validator, IOC checker — stdlib only - Monitoring: Prometheus alerting rules (node + service), Grafana SOC dashboard - Plans: 10-item roadmap (SIEM, honeypots, threat intel, purple team, etc.) - Professional README landing page with tech stack badges and MIT license All content uses fictional IPs/hostnames — no real infrastructure data exposed. Co-authored-by: Claude Opus 4.6 --- LICENSE | 21 ++ automation/README.md | 38 ++ automation/scripts/backup-validator.sh | 141 ++++++++ automation/scripts/ioc-checker.py | 181 ++++++++++ automation/scripts/log-parser.py | 179 ++++++++++ detection/README.md | 38 ++ .../alert-logic/log-correlation-patterns.md | 151 ++++++++ detection/alert-logic/threshold-vs-anomaly.md | 56 +++ detection/sigma-rules/brute-force-auth.yml | 44 +++ .../sigma-rules/lateral-movement-smb.yml | 86 +++++ .../sigma-rules/privilege-escalation-sudo.yml | 83 +++++ .../sigma-rules/suspicious-powershell.yml | 75 ++++ hardening/README.md | 42 +++ hardening/ansible-roles/common/tasks/main.yml | 77 +++++ .../ansible-roles/firewall/defaults/main.yml | 28 ++ .../ansible-roles/firewall/handlers/main.yml | 4 + .../ansible-roles/firewall/tasks/main.yml | 82 +++++ .../ssh-hardening/defaults/main.yml | 52 +++ .../ssh-hardening/handlers/main.yml | 10 + .../ssh-hardening/tasks/main.yml | 207 +++++++++++ hardening/baselines/linux-baseline.md | 324 ++++++++++++++++++ hardening/baselines/ssh-hardening.md | 182 ++++++++++ monitoring/README.md | 29 ++ monitoring/dashboards/soc-overview.json | 174 ++++++++++ monitoring/prometheus-rules/node-alerts.yml | 56 +++ .../prometheus-rules/service-alerts.yml | 52 +++ plans/README.md | 103 ++++++ playbooks/README.md | 38 ++ playbooks/incident-triage.md | 100 ++++++ playbooks/initial-incident-triage.md | 57 --- playbooks/malware-containment.md | 142 ++++++++ playbooks/phishing-response.md | 109 ++++++ playbooks/unauthorized-access.md | 121 +++++++ 33 files changed, 3025 insertions(+), 57 deletions(-) create mode 100644 LICENSE create mode 100644 automation/README.md create mode 100644 automation/scripts/backup-validator.sh create mode 100644 automation/scripts/ioc-checker.py create mode 100644 automation/scripts/log-parser.py create mode 100644 detection/README.md create mode 100644 detection/alert-logic/log-correlation-patterns.md create mode 100644 detection/alert-logic/threshold-vs-anomaly.md create mode 100644 detection/sigma-rules/brute-force-auth.yml create mode 100644 detection/sigma-rules/lateral-movement-smb.yml create mode 100644 detection/sigma-rules/privilege-escalation-sudo.yml create mode 100644 detection/sigma-rules/suspicious-powershell.yml create mode 100644 hardening/README.md create mode 100644 hardening/ansible-roles/common/tasks/main.yml create mode 100644 hardening/ansible-roles/firewall/defaults/main.yml create mode 100644 hardening/ansible-roles/firewall/handlers/main.yml create mode 100644 hardening/ansible-roles/firewall/tasks/main.yml create mode 100644 hardening/ansible-roles/ssh-hardening/defaults/main.yml create mode 100644 hardening/ansible-roles/ssh-hardening/handlers/main.yml create mode 100644 hardening/ansible-roles/ssh-hardening/tasks/main.yml create mode 100644 hardening/baselines/linux-baseline.md create mode 100644 hardening/baselines/ssh-hardening.md create mode 100644 monitoring/README.md create mode 100644 monitoring/dashboards/soc-overview.json create mode 100644 monitoring/prometheus-rules/node-alerts.yml create mode 100644 monitoring/prometheus-rules/service-alerts.yml create mode 100644 plans/README.md create mode 100644 playbooks/README.md create mode 100644 playbooks/incident-triage.md delete mode 100644 playbooks/initial-incident-triage.md create mode 100644 playbooks/malware-containment.md create mode 100644 playbooks/phishing-response.md create mode 100644 playbooks/unauthorized-access.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59a49dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Steven J. Vik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/automation/README.md b/automation/README.md new file mode 100644 index 0000000..afe9934 --- /dev/null +++ b/automation/README.md @@ -0,0 +1,38 @@ +# Automation + +Python and Bash scripts for operational security tasks in the SOC lab. + +## Purpose + +These scripts automate repetitive SOC and sysadmin workflows: log analysis, backup validation, and IOC triage. They use only standard library dependencies — no pip installs required. + +## Scripts + +| Script | Language | Purpose | +|--------|----------|---------| +| `log-parser.py` | Python 3 | Parse auth.log for failed/successful logins, summarize by IP and user | +| `backup-validator.sh` | Bash | Verify backup recency, size, and optional checksum integrity | +| `ioc-checker.py` | Python 3 | Triage IOCs (IPs, domains, hashes) against a local blocklist | + +## Usage + +All scripts are standalone and self-documenting: + +```bash +# Log analysis +python3 scripts/log-parser.py --file /var/log/auth.log --format table --top-n 10 + +# Backup validation +bash scripts/backup-validator.sh /backup/daily/ 24 + +# IOC triage +python3 scripts/ioc-checker.py --ioc-file suspicious.txt --blocklist blocklists/known-bad.txt +``` + +Run any script with `--help` or `-h` for full usage details. + +## Design Principles + +- **No external dependencies.** Python stdlib only, POSIX-compatible Bash. These run on any Linux host without setup. +- **Structured output.** All scripts support multiple output formats (table, JSON, CSV) for integration with other tools. +- **Fictional example data.** All IPs, hostnames, and hashes in examples and comments use non-routable or clearly fictional values. diff --git a/automation/scripts/backup-validator.sh b/automation/scripts/backup-validator.sh new file mode 100644 index 0000000..8edaae9 --- /dev/null +++ b/automation/scripts/backup-validator.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# +# backup-validator.sh -- Validate backup recency, size, and integrity. +# +# Checks that backup files exist in a target directory, are not older +# than a configurable threshold, and are not suspiciously small. +# Optionally verifies file integrity against a SHA-256 checksum file. +# +# Usage: +# bash backup-validator.sh [max_age_hours] +# +# Examples: +# bash backup-validator.sh /backup/daily/ +# bash backup-validator.sh /backup/daily/ 48 +# bash backup-validator.sh /backup/weekly/ 168 +# +# Exit codes: +# 0 -- All checks passed +# 1 -- One or more checks failed +# 2 -- Usage error +# + +set -euo pipefail + +DEFAULT_MAX_AGE_HOURS=24 +MIN_FILE_SIZE_BYTES=1024 +CHECKSUM_FILE="checksums.sha256" + +if [[ -t 1 ]]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; NC='' +fi + +pass() { echo -e " ${GREEN}[PASS]${NC} $*"; } +fail() { echo -e " ${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); } +warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; } +info() { echo -e " [INFO] $*"; } + +usage() { + echo "Usage: $0 [max_age_hours]" + echo " backup_directory Path to the directory containing backup files" + echo " max_age_hours Maximum acceptable age in hours (default: ${DEFAULT_MAX_AGE_HOURS})" + exit 2 +} + +if [[ $# -lt 1 ]]; then usage; fi + +BACKUP_DIR="$1" +MAX_AGE_HOURS="${2:-$DEFAULT_MAX_AGE_HOURS}" +FAILURES=0 + +echo "============================================" +echo " BACKUP VALIDATION REPORT" +echo "============================================" +echo "" +info "Backup directory: ${BACKUP_DIR}" +info "Max age threshold: ${MAX_AGE_HOURS} hours" +info "Min file size: ${MIN_FILE_SIZE_BYTES} bytes" +echo "" + +echo "--- Directory Check ---" +if [[ -d "${BACKUP_DIR}" ]]; then + pass "Backup directory exists" +else + fail "Backup directory does not exist: ${BACKUP_DIR}" + echo "Result: FAILED (${FAILURES} issue(s))"; exit 1 +fi + +echo "" +echo "--- File Presence Check ---" +FILE_COUNT=$(find "${BACKUP_DIR}" -maxdepth 1 -type f | wc -l) +if [[ "${FILE_COUNT}" -gt 0 ]]; then + pass "Found ${FILE_COUNT} file(s) in backup directory" +else + fail "Backup directory is empty" + echo "Result: FAILED (${FAILURES} issue(s))"; exit 1 +fi + +echo "" +echo "--- File Age Check (max ${MAX_AGE_HOURS}h) ---" +MAX_AGE_SECONDS=$((MAX_AGE_HOURS * 3600)) +NOW=$(date +%s) +while IFS= read -r -d '' file; do + filename=$(basename "${file}") + file_mtime=$(stat -c '%Y' "${file}" 2>/dev/null || stat -f '%m' "${file}" 2>/dev/null) + age_seconds=$((NOW - file_mtime)) + age_hours=$((age_seconds / 3600)) + if [[ ${age_seconds} -gt ${MAX_AGE_SECONDS} ]]; then + fail "${filename} is ${age_hours}h old (threshold: ${MAX_AGE_HOURS}h)" + else + pass "${filename} is ${age_hours}h old" + fi +done < <(find "${BACKUP_DIR}" -maxdepth 1 -type f -print0) + +echo "" +echo "--- File Size Check (min ${MIN_FILE_SIZE_BYTES} bytes) ---" +while IFS= read -r -d '' file; do + filename=$(basename "${file}") + file_size=$(stat -c '%s' "${file}" 2>/dev/null || stat -f '%z' "${file}" 2>/dev/null) + if [[ ${file_size} -lt ${MIN_FILE_SIZE_BYTES} ]]; then + fail "${filename} is ${file_size} bytes (suspiciously small)" + else + if [[ ${file_size} -ge 1073741824 ]]; then + hr_size="$(echo "scale=1; ${file_size}/1073741824" | bc) GB" + elif [[ ${file_size} -ge 1048576 ]]; then + hr_size="$(echo "scale=1; ${file_size}/1048576" | bc) MB" + elif [[ ${file_size} -ge 1024 ]]; then + hr_size="$(echo "scale=1; ${file_size}/1024" | bc) KB" + else + hr_size="${file_size} bytes" + fi + pass "${filename} -- ${hr_size}" + fi +done < <(find "${BACKUP_DIR}" -maxdepth 1 -type f -print0) + +echo "" +echo "--- Integrity Check ---" +CHECKSUM_PATH="${BACKUP_DIR%/}/${CHECKSUM_FILE}" +if [[ -f "${CHECKSUM_PATH}" ]]; then + info "Found checksum file: ${CHECKSUM_FILE}" + pushd "${BACKUP_DIR}" > /dev/null + if sha256sum --check --quiet "${CHECKSUM_FILE}" 2>/dev/null; then + pass "All checksums verified" + else + fail "Checksum verification failed -- possible corruption" + fi + popd > /dev/null +else + warn "No checksum file found (${CHECKSUM_FILE}) -- skipping integrity check" +fi + +echo "" +echo "============================================" +if [[ ${FAILURES} -eq 0 ]]; then + echo -e " Result: ${GREEN}ALL CHECKS PASSED${NC}" +else + echo -e " Result: ${RED}FAILED -- ${FAILURES} issue(s) found${NC}" +fi +echo "============================================" +exit $( [[ ${FAILURES} -eq 0 ]] && echo 0 || echo 1 ) \ No newline at end of file diff --git a/automation/scripts/ioc-checker.py b/automation/scripts/ioc-checker.py new file mode 100644 index 0000000..dff9de8 --- /dev/null +++ b/automation/scripts/ioc-checker.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +ioc-checker.py -- Triage Indicators of Compromise against a local blocklist. + +Reads IOCs (IP addresses, domains, file hashes) from a file or stdin and +checks each against a local blocklist. Outputs a triage report showing +matched and unmatched indicators with confidence levels. + +Usage: + python3 ioc-checker.py --ioc-file suspicious.txt --blocklist known-bad.txt + echo "10.0.50.100" | python3 ioc-checker.py --blocklist known-bad.txt + python3 ioc-checker.py --ioc-file iocs.txt --blocklist known-bad.txt --format json + +Blocklist format (one entry per line): + 10.0.50.100 + evil-domain.example.com + d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592 + +Dependencies: Python 3.6+ standard library only. +""" + +import argparse +import csv +import io +import json +import re +import sys +from collections import defaultdict + + +IPV4_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") +MD5_RE = re.compile(r"^[a-fA-F0-9]{32}$") +SHA1_RE = re.compile(r"^[a-fA-F0-9]{40}$") +SHA256_RE = re.compile(r"^[a-fA-F0-9]{64}$") +DOMAIN_RE = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$") + + +def classify_ioc(value): + """Determine the type of an IOC value.""" + if IPV4_RE.match(value): + return "ipv4" + if SHA256_RE.match(value): + return "sha256" + if SHA1_RE.match(value): + return "sha1" + if MD5_RE.match(value): + return "md5" + if DOMAIN_RE.match(value): + return "domain" + return "unknown" + + +def load_blocklist(filepath): + """Load blocklist entries into a set.""" + entries = set() + try: + with open(filepath, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + entries.add(line.lower()) + except FileNotFoundError: + print("Error: Blocklist not found: {}".format(filepath), file=sys.stderr) + sys.exit(1) + return entries + + +def load_iocs(filepath=None): + """Load IOCs from file or stdin.""" + iocs = [] + source = sys.stdin if filepath is None else open(filepath, "r", encoding="utf-8") + try: + for line in source: + line = line.strip() + if line and not line.startswith("#"): + iocs.append(line) + finally: + if filepath is not None: + source.close() + return iocs + + +def check_iocs(iocs, blocklist): + """Check each IOC against the blocklist.""" + results = [] + for ioc in iocs: + ioc_type = classify_ioc(ioc) + matched = ioc.lower() in blocklist + if matched: + if ioc_type in ("sha256", "sha1"): + confidence = "high" + elif ioc_type in ("md5", "ipv4", "domain"): + confidence = "medium" + else: + confidence = "low" + else: + confidence = "n/a" + results.append({"ioc": ioc, "type": ioc_type, "matched": matched, "confidence": confidence}) + return results + + +def format_table(results): + """Format results as a human-readable triage report.""" + matched = [r for r in results if r["matched"]] + unmatched = [r for r in results if not r["matched"]] + lines = [] + lines.append("=" * 72) + lines.append("IOC TRIAGE REPORT") + lines.append("=" * 72) + lines.append("") + lines.append(" Total IOCs checked: {}".format(len(results))) + lines.append(" Matched (blocked): {}".format(len(matched))) + lines.append(" Unmatched (clean): {}".format(len(unmatched))) + lines.append("") + if matched: + lines.append("-" * 72) + lines.append("MATCHED -- Present in blocklist") + lines.append(" {:<45} {:<10} {:<10}".format("IOC", "Type", "Confidence")) + lines.append(" {} {} {}".format("-" * 45, "-" * 10, "-" * 10)) + for r in matched: + lines.append(" {:<45} {:<10} {:<10}".format(r["ioc"], r["type"], r["confidence"])) + lines.append("") + if unmatched: + lines.append("-" * 72) + lines.append("UNMATCHED -- Not in blocklist") + lines.append(" {:<45} {:<10}".format("IOC", "Type")) + lines.append(" {} {}".format("-" * 45, "-" * 10)) + for r in unmatched: + lines.append(" {:<45} {:<10}".format(r["ioc"], r["type"])) + lines.append("") + type_counts = defaultdict(lambda: {"total": 0, "matched": 0}) + for r in results: + type_counts[r["type"]]["total"] += 1 + if r["matched"]: + type_counts[r["type"]]["matched"] += 1 + lines.append("-" * 72) + lines.append("SUMMARY BY TYPE") + lines.append(" {:<12} {:>8} {:>10} {:>10}".format("Type", "Total", "Matched", "Hit Rate")) + lines.append(" {} {} {} {}".format("-" * 12, "-" * 8, "-" * 10, "-" * 10)) + for ioc_type, counts in sorted(type_counts.items()): + rate = "{}%".format(int(counts["matched"] / counts["total"] * 100)) if counts["total"] > 0 else "0%" + lines.append(" {:<12} {:>8} {:>10} {:>10}".format(ioc_type, counts["total"], counts["matched"], rate)) + lines.append("") + lines.append("=" * 72) + return "\n".join(lines) + + +def format_json(results): + """Format results as JSON.""" + matched = [r for r in results if r["matched"]] + unmatched = [r for r in results if not r["matched"]] + return json.dumps({"total": len(results), "matched_count": len(matched), "unmatched_count": len(unmatched), "matched": matched, "unmatched": unmatched}, indent=2) + + +def format_csv(results): + """Format results as CSV.""" + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=["ioc", "type", "matched", "confidence"]) + writer.writeheader() + writer.writerows(results) + return output.getvalue() + + +def main(): + parser = argparse.ArgumentParser(description="Triage IOCs against a local blocklist.") + parser.add_argument("--ioc-file", default=None, help="File containing IOCs. Reads stdin if omitted.") + parser.add_argument("--blocklist", "-b", required=True, help="Path to blocklist file") + parser.add_argument("--format", choices=["table", "json", "csv"], default="table", help="Output format (default: table)") + args = parser.parse_args() + blocklist = load_blocklist(args.blocklist) + iocs = load_iocs(args.ioc_file) + if not iocs: + print("No IOCs provided. Use --ioc-file or pipe via stdin.", file=sys.stderr) + sys.exit(1) + results = check_iocs(iocs, blocklist) + formatters = {"table": format_table, "json": format_json, "csv": format_csv} + print(formatters[args.format](results)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/automation/scripts/log-parser.py b/automation/scripts/log-parser.py new file mode 100644 index 0000000..8331b92 --- /dev/null +++ b/automation/scripts/log-parser.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +log-parser.py -- Parse auth.log for authentication events. + +Extracts failed and successful SSH login attempts from syslog-format +auth.log files. Produces a summary of top offending IPs, targeted +usernames, and a timeline of events. + +Usage: + python3 log-parser.py --file /var/log/auth.log + python3 log-parser.py --file /var/log/auth.log --format json --top-n 20 + python3 log-parser.py --file auth.log --format csv > report.csv + +Output formats: table (default), json, csv + +Dependencies: Python 3.6+ standard library only. +""" + +import argparse +import csv +import io +import json +import re +import sys +from collections import Counter +from datetime import datetime + + +FAILED_PASSWORD_RE = re.compile( + r"^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+sshd\[\d+\]:\s+" + r"Failed password for (?:invalid user )?(\S+)\s+from\s+(\S+)" +) + +ACCEPTED_RE = re.compile( + r"^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+sshd\[\d+\]:\s+" + r"Accepted (?:publickey|password) for (\S+)\s+from\s+(\S+)" +) + +INVALID_USER_RE = re.compile( + r"^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+sshd\[\d+\]:\s+" + r"Invalid user (\S+)\s+from\s+(\S+)" +) + + +def parse_log(filepath): + events = {"failed": [], "accepted": [], "invalid_user": []} + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + match = FAILED_PASSWORD_RE.search(line) + if match: + events["failed"].append({"timestamp": match.group(1), "user": match.group(2), "ip": match.group(3), "type": "failed_password"}) + continue + match = ACCEPTED_RE.search(line) + if match: + events["accepted"].append({"timestamp": match.group(1), "user": match.group(2), "ip": match.group(3), "type": "accepted"}) + continue + match = INVALID_USER_RE.search(line) + if match: + events["invalid_user"].append({"timestamp": match.group(1), "user": match.group(2), "ip": match.group(3), "type": "invalid_user"}) + return events + + +def build_summary(events, top_n): + failed_ips = Counter(e["ip"] for e in events["failed"]) + failed_users = Counter(e["user"] for e in events["failed"]) + accepted_ips = Counter(e["ip"] for e in events["accepted"]) + invalid_users = Counter(e["user"] for e in events["invalid_user"]) + return { + "total_failed": len(events["failed"]), + "total_accepted": len(events["accepted"]), + "total_invalid_user": len(events["invalid_user"]), + "unique_failed_ips": len(failed_ips), + "top_failed_ips": failed_ips.most_common(top_n), + "top_failed_users": failed_users.most_common(top_n), + "top_accepted_ips": accepted_ips.most_common(top_n), + "top_invalid_users": invalid_users.most_common(top_n), + } + + +def format_table(summary): + lines = [] + lines.append("=" * 60) + lines.append("AUTH LOG ANALYSIS REPORT") + lines.append("=" * 60) + lines.append("") + lines.append(" Total failed login attempts: {}".format(summary["total_failed"])) + lines.append(" Total successful logins: {}".format(summary["total_accepted"])) + lines.append(" Total invalid user attempts: {}".format(summary["total_invalid_user"])) + lines.append(" Unique source IPs (failed): {}".format(summary["unique_failed_ips"])) + lines.append("") + lines.append("-" * 60) + lines.append("TOP FAILED SOURCE IPs") + lines.append(" {:<25} {:>10}".format("IP Address", "Attempts")) + lines.append(" {} {}".format("-" * 25, "-" * 10)) + for ip, count in summary["top_failed_ips"]: + lines.append(" {:<25} {:>10}".format(ip, count)) + lines.append("") + lines.append("-" * 60) + lines.append("TOP TARGETED USERNAMES (failed)") + lines.append(" {:<25} {:>10}".format("Username", "Attempts")) + lines.append(" {} {}".format("-" * 25, "-" * 10)) + for user, count in summary["top_failed_users"]: + lines.append(" {:<25} {:>10}".format(user, count)) + lines.append("") + lines.append("-" * 60) + lines.append("SUCCESSFUL LOGIN SOURCES") + lines.append(" {:<25} {:>10}".format("IP Address", "Logins")) + lines.append(" {} {}".format("-" * 25, "-" * 10)) + for ip, count in summary["top_accepted_ips"]: + lines.append(" {:<25} {:>10}".format(ip, count)) + lines.append("") + if summary["top_invalid_users"]: + lines.append("-" * 60) + lines.append("TOP INVALID USERNAMES (reconnaissance)") + lines.append(" {:<25} {:>10}".format("Username", "Attempts")) + lines.append(" {} {}".format("-" * 25, "-" * 10)) + for user, count in summary["top_invalid_users"]: + lines.append(" {:<25} {:>10}".format(user, count)) + lines.append("") + lines.append("=" * 60) + return "\n".join(lines) + + +def format_json(summary): + output = { + "total_failed": summary["total_failed"], + "total_accepted": summary["total_accepted"], + "total_invalid_user": summary["total_invalid_user"], + "unique_failed_ips": summary["unique_failed_ips"], + "top_failed_ips": [{"ip": ip, "count": c} for ip, c in summary["top_failed_ips"]], + "top_failed_users": [{"user": u, "count": c} for u, c in summary["top_failed_users"]], + "top_accepted_ips": [{"ip": ip, "count": c} for ip, c in summary["top_accepted_ips"]], + "top_invalid_users": [{"user": u, "count": c} for u, c in summary["top_invalid_users"]], + } + return json.dumps(output, indent=2) + + +def format_csv(summary): + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(["section", "key", "value"]) + writer.writerow(["summary", "total_failed", summary["total_failed"]]) + writer.writerow(["summary", "total_accepted", summary["total_accepted"]]) + writer.writerow(["summary", "total_invalid_user", summary["total_invalid_user"]]) + writer.writerow(["summary", "unique_failed_ips", summary["unique_failed_ips"]]) + for ip, count in summary["top_failed_ips"]: + writer.writerow(["top_failed_ips", ip, count]) + for user, count in summary["top_failed_users"]: + writer.writerow(["top_failed_users", user, count]) + for ip, count in summary["top_accepted_ips"]: + writer.writerow(["top_accepted_ips", ip, count]) + return output.getvalue() + + +def main(): + parser = argparse.ArgumentParser( + description="Parse auth.log for SSH authentication events.", + epilog="Example: python3 log-parser.py --file /var/log/auth.log --format table", + ) + parser.add_argument("--file", "-f", required=True, help="Path to auth.log file") + parser.add_argument("--format", choices=["table", "json", "csv"], default="table", help="Output format (default: table)") + parser.add_argument("--top-n", type=int, default=10, help="Number of top entries to show (default: 10)") + args = parser.parse_args() + try: + events = parse_log(args.file) + except FileNotFoundError: + print("Error: File not found: {}".format(args.file), file=sys.stderr) + sys.exit(1) + except PermissionError: + print("Error: Permission denied: {}".format(args.file), file=sys.stderr) + sys.exit(1) + summary = build_summary(events, args.top_n) + formatters = {"table": format_table, "json": format_json, "csv": format_csv} + print(formatters[args.format](summary)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/detection/README.md b/detection/README.md new file mode 100644 index 0000000..cfa165b --- /dev/null +++ b/detection/README.md @@ -0,0 +1,38 @@ +# Detection Engineering + +Detection rules, correlation logic, and alert tuning methodology for the SOC lab. + +## Philosophy + +Good detection is not about writing the most rules. It is about writing the *right* rules and keeping them honest. + +**Defense in depth for alerting** means no single detection layer is trusted alone. Network-level detections miss encrypted C2. Endpoint detections miss living-off-the-land if the binary is trusted. Log-based detections miss what is not logged. Layering these creates overlap where a miss at one layer is caught by another. + +**Signal-to-noise ratio** is the metric that matters most. A SOC drowning in false positives is functionally blind -- analysts stop reading alerts, and real threats hide in the noise. Every rule must earn its place. If a rule generates more noise than signal after tuning, it gets removed or rearchitected, not ignored. + +**Tuning is continuous.** Detection rules are not write-once artifacts. Environments change, baselines shift, and adversaries adapt. Every rule should have a tuning history: when it was last reviewed, what the false positive rate is, and what adjustments were made. + +## Why Sigma + +Rules in this repo use the [Sigma](https://github.com/SigmaHQ/sigma) format because: + +- **Vendor-neutral:** Sigma rules describe *what* to detect, not *how* in a specific SIEM. They convert to Splunk SPL, Elastic KQL, Microsoft Sentinel, and others via the `sigma-cli` toolchain. +- **Community standard:** The SigmaHQ repository contains thousands of peer-reviewed rules. Writing in the same format means contributing back is trivial and consuming community rules requires no translation. +- **Version-controllable:** YAML files diff cleanly in Git, making rule changes auditable. +- **Testable:** Sigma rules can be validated against log samples before deployment, reducing the "deploy and hope" cycle. + +## Structure + +``` +detection/ +├── sigma-rules/ # Individual Sigma detection rules +├── alert-logic/ # Correlation patterns and threshold documentation +└── README.md # This file +``` + +## Rule Lifecycle + +1. **Draft** -- rule written, not yet tested against real logs +2. **Test** -- deployed in detection-only mode (no alerting), measuring false positive rate +3. **Active** -- alerting enabled, tuned to acceptable noise level +4. **Deprecated** -- superseded or no longer relevant, kept for reference diff --git a/detection/alert-logic/log-correlation-patterns.md b/detection/alert-logic/log-correlation-patterns.md new file mode 100644 index 0000000..6dc3f3f --- /dev/null +++ b/detection/alert-logic/log-correlation-patterns.md @@ -0,0 +1,151 @@ +# Log Correlation Patterns + +Multi-source event correlation for detecting attack chains that no single rule catches alone. + +## Why Correlation Matters + +Individual events are ambiguous. A failed login is noise. A failed login followed by a successful login from a new IP, followed by admin share access on a different host, followed by a large outbound transfer -- that is an attack chain. Correlation connects the dots across log sources and time windows. + +The patterns below describe detection logic independent of any specific SIEM. They define the sequence, the join keys, and the time windows. Implementation details (SPL, KQL, YARA-L) are environment-specific. + +--- + +## Pattern 1: Credential Compromise to Lateral Movement + +**Chain:** Auth failure burst --> Successful login --> Access to new host + +**Logic:** +``` +STAGE 1: Brute force signal + WHERE event_type = "authentication" + AND outcome = "failure" + AND count(event) > 10 within 10 minutes + AND grouped by target_username, source_ip + +STAGE 2: Successful login (same account) + WHERE event_type = "authentication" + AND outcome = "success" + AND target_username = STAGE_1.target_username + AND timestamp within 60 minutes of STAGE_1.last_event + +STAGE 3: Lateral movement (same account, different host) + WHERE event_type IN ("smb_share_access", "rdp_login", "ssh_login") + AND username = STAGE_1.target_username + AND destination_host != STAGE_2.host + AND timestamp within 4 hours of STAGE_2.timestamp +``` + +**Join key:** `target_username` +**Time window:** Stage 1 to Stage 3 within 4 hours +**Severity:** High if Stage 3 fires, Medium if only Stage 1 + 2 + +--- + +## Pattern 2: Phishing to Payload Execution + +**Chain:** Email delivery --> User click --> Process execution --> Outbound C2 + +**Logic:** +``` +STAGE 1: Suspicious email delivered + WHERE event_source = "email_gateway" + AND verdict IN ("suspicious", "malicious") + AND action = "delivered" + +STAGE 2: URL click or attachment open (same user) + WHERE event_source IN ("proxy", "edr") + AND (url IN STAGE_1.extracted_urls + OR file_hash IN STAGE_1.attachment_hashes) + AND user = STAGE_1.recipient + AND timestamp within 24 hours of STAGE_1.timestamp + +STAGE 3: Suspicious process execution (same host) + WHERE event_source = "edr" + AND parent_process IN ("outlook.exe", "chrome.exe", "firefox.exe", "msedge.exe") + AND process IN ("powershell.exe", "cmd.exe", "wscript.exe", "mshta.exe") + AND host = STAGE_2.host + AND timestamp within 5 minutes of STAGE_2.timestamp + +STAGE 4: Outbound connection to uncategorized/new domain + WHERE event_source IN ("proxy", "dns", "firewall") + AND destination NOT IN known_good_domains + AND source_host = STAGE_3.host + AND timestamp within 30 minutes of STAGE_3.timestamp +``` + +**Join keys:** `recipient/user`, `host`, `extracted IOCs` +**Time window:** Stage 1 to Stage 4 within 24 hours +**Severity:** Critical if Stage 4 fires, High at Stage 3 + +--- + +## Pattern 3: Privilege Escalation to Data Access + +**Chain:** Standard user action --> Privilege gain --> Sensitive data access + +**Logic:** +``` +STAGE 1: Privilege change detected + WHERE event_type IN ("group_membership_change", "role_assignment", "sudo_root") + AND target_user NOT IN admin_whitelist + AND change NOT IN approved_change_tickets + +STAGE 2: Access to sensitive resources (same account) + WHERE event_type IN ("file_access", "database_query", "share_access") + AND resource_classification IN ("confidential", "restricted") + AND user = STAGE_1.target_user + AND timestamp within 2 hours of STAGE_1.timestamp + AND user has NO prior access history to this resource + +STAGE 3: Data exfiltration signal (same host or account) + WHERE event_type IN ("large_upload", "usb_write", "email_attachment", "cloud_sync") + AND data_volume > baseline_threshold + AND (user = STAGE_1.target_user OR host = STAGE_2.host) + AND timestamp within 4 hours of STAGE_2.timestamp +``` + +**Join key:** `target_user`, `host` +**Time window:** Stage 1 to Stage 3 within 6 hours +**Severity:** Critical at Stage 3, High at Stage 2 + +--- + +## Pattern 4: Service Account Abuse + +**Chain:** Service account interactive login --> Unusual activity --> Persistence + +**Logic:** +``` +STAGE 1: Service account used interactively + WHERE event_type = "authentication" + AND account_type = "service" + AND logon_type IN ("interactive", "remote_interactive") + -- Service accounts should never log in interactively + +STAGE 2: Unusual activity from service account + WHERE (event_type = "process_creation" + AND user = STAGE_1.account + AND process NOT IN expected_service_processes) + OR (event_type = "authentication" + AND user = STAGE_1.account + AND destination_host NOT IN expected_service_hosts) + AND timestamp within 2 hours of STAGE_1.timestamp + +STAGE 3: Persistence mechanism created + WHERE event_type IN ("scheduled_task_created", "service_installed", "registry_run_key") + AND user = STAGE_1.account + AND timestamp within 4 hours of STAGE_1.timestamp +``` + +**Join key:** `service account name` +**Time window:** Stage 1 to Stage 3 within 4 hours +**Severity:** High at Stage 1 (service accounts should not log in interactively), Critical at Stage 3 + +--- + +## Implementation Notes + +- **Lookback windows** should be tuned to your environment. Faster networks and smaller orgs can tighten windows. Larger environments with async log collection may need wider windows. +- **Whitelists** (admin users, known hosts, expected processes) must be maintained as living documents. Stale whitelists create blind spots. +- **Join key reliability** depends on consistent identity across log sources. If your SIEM normalizes `username` differently across sources, correlation breaks silently. +- **Test with red team data.** Correlation rules are hard to validate with production logs alone because real attack chains are rare. Use atomic red team tests or purple team exercises to generate the event sequences. diff --git a/detection/alert-logic/threshold-vs-anomaly.md b/detection/alert-logic/threshold-vs-anomaly.md new file mode 100644 index 0000000..ceffb6b --- /dev/null +++ b/detection/alert-logic/threshold-vs-anomaly.md @@ -0,0 +1,56 @@ +# Threshold vs. Anomaly Detection + +When to use static thresholds and when to use baseline-driven anomaly detection. + +## Static Thresholds + +A fixed value that fires when crossed. Simple, predictable, easy to explain. + +**Use thresholds when:** +- The behavior has a clear "never normal" boundary (e.g., 50 failed logins in 5 minutes) +- The baseline is stable and well-understood +- You need deterministic, reproducible alerts (compliance, audit) +- False positive tuning is straightforward (adjust the number) + +**Examples:** +| Rule | Threshold | Rationale | +|------|-----------|-----------| +| Failed logins from single IP | > 10 in 10 min | Legitimate users mistype 1-3 times, not 10 | +| Outbound data transfer | > 500 MB to single external IP in 1 hour | Normal uploads are small; bulk transfer is unusual | +| Account lockouts | > 3 in 1 hour for same account | Indicates brute force or misconfigured integration | +| New admin accounts created | > 0 outside change window | Admin account creation should always be planned | + +**Strengths:** Transparent, debuggable, no training period, no math. +**Weakness:** Cannot adapt. A threshold set for Monday traffic will false-positive on Friday's batch jobs, or miss a slow-and-low attack that stays just under the line. + +## Anomaly / Baseline Detection + +Compare current behavior against a learned baseline. Fires on deviation, not a fixed number. + +**Use anomaly detection when:** +- "Normal" varies by time of day, day of week, or user role +- You are looking for unknown-unknowns (novel attack patterns) +- The volume or pattern of activity matters more than any single event +- Static thresholds would require constant manual tuning + +**Examples:** +| Rule | Baseline | Deviation | +|------|----------|-----------| +| User login hours | User typically logs in 08:00-18:00 weekdays | Login at 03:00 on a Saturday | +| DNS query volume per host | Host averages 200 queries/hour | Spike to 5,000 queries/hour (possible DNS tunneling) | +| Process execution on server | Server runs 12 known processes | New process `nc.exe` appears for the first time | +| Data access per user | User accesses 10-20 files/day | User accesses 500 files in 2 hours | + +**Strengths:** Adapts to environment, catches slow attacks, surfaces things you did not think to write a rule for. +**Weakness:** Requires training period, produces opaque alerts ("this is unusual" is not actionable by itself), baselines drift, seasonal changes cause false positives. + +## The Practical Answer + +Use both. They cover different gaps. + +- **Thresholds** are your guardrails -- hard limits that should never be crossed regardless of context. +- **Anomalies** are your peripheral vision -- surfacing things that are unusual enough to warrant a look. + +Layer them: a threshold catches the brute force spray, the anomaly catches the single successful login from a country the user has never visited. Neither alone is sufficient. + +**One rule of thumb:** If you can define "bad" with a number, use a threshold. If you can only define "weird compared to normal," use anomaly detection. If you cannot define either, you do not understand the data well enough to alert on it yet -- start with dashboards and manual review. diff --git a/detection/sigma-rules/brute-force-auth.yml b/detection/sigma-rules/brute-force-auth.yml new file mode 100644 index 0000000..ecae411 --- /dev/null +++ b/detection/sigma-rules/brute-force-auth.yml @@ -0,0 +1,44 @@ +title: Brute Force Authentication Attempts +id: 3f8a1c2e-9b4d-4e7f-a6c1-8d2e5f0b3a7c +status: test +description: > + Detects multiple failed authentication attempts from a single source within a short + time window, indicating a potential brute force or credential stuffing attack. + Threshold is tuned to reduce false positives from mistyped passwords while catching + automated attacks. +references: + - https://attack.mitre.org/techniques/T1110/ + - https://nvd.nist.gov/800-53/Rev4/control/AC-7 +author: Steven Vik +date: 2026/03/10 +tags: + - attack.credential_access + - attack.t1110 + - attack.t1110.001 + - attack.t1110.003 +logsource: + category: authentication + product: windows +detection: + selection: + EventID: + - 4625 + LogonType: + - 2 + - 3 + - 10 + filter_service_accounts: + TargetUserName|endswith: + - '$' + condition: selection and not filter_service_accounts | count(TargetUserName) by SourceAddress > 10 + timeframe: 10m +fields: + - TargetUserName + - SourceAddress + - LogonType + - WorkstationName +falsepositives: + - Misconfigured service accounts with wrong credentials + - Vulnerability scanners or IT tools performing authenticated checks + - Users with expired passwords repeatedly failing SSO +level: medium diff --git a/detection/sigma-rules/lateral-movement-smb.yml b/detection/sigma-rules/lateral-movement-smb.yml new file mode 100644 index 0000000..31406c7 --- /dev/null +++ b/detection/sigma-rules/lateral-movement-smb.yml @@ -0,0 +1,86 @@ +title: Lateral Movement via SMB Admin Shares +id: a4c9e6f2-1b3d-4a8e-9c7f-5d2b0e6a1f8c +status: test +description: > + Detects access to administrative SMB shares (C$, ADMIN$, IPC$) and PsExec-like + behavior between internal hosts. While admin share access has legitimate uses, + it is a primary lateral movement vector and should be monitored, especially + between workstations or from unexpected source hosts. +references: + - https://attack.mitre.org/techniques/T1021/002/ + - https://attack.mitre.org/techniques/T1570/ +author: Steven Vik +date: 2026/03/10 +tags: + - attack.lateral_movement + - attack.t1021.002 + - attack.t1570 +logsource: + product: windows + service: security +detection: + selection_share_access: + EventID: 5140 + ShareName|contains: + - 'C$' + - 'ADMIN$' + filter_trusted_sources: + IpAddress|startswith: + - '10.0.0.10' + - '10.0.0.11' + condition: selection_share_access and not filter_trusted_sources +fields: + - SubjectUserName + - IpAddress + - ShareName + - RelativeTargetName + - AccessMask +falsepositives: + - Legitimate PsExec use by authorized administrators (should be documented) + - IT management tools that deploy via admin shares + - SCCM or similar endpoint management creating temporary services +level: high +--- +title: PsExec-like Service Installation +id: b5d0f7a3-2c4e-5b9f-0d8a-6e3c1f7b2a9d +status: test +description: > + Detects the creation of services with characteristics matching PsExec or similar + remote execution tools. PsExec installs a temporary service (PSEXESVC) on the + target host to execute commands remotely. +references: + - https://attack.mitre.org/software/S0029/ +author: Steven Vik +date: 2026/03/10 +tags: + - attack.lateral_movement + - attack.execution + - attack.t1021.002 + - attack.t1569.002 +logsource: + product: windows + service: system +detection: + selection_service: + EventID: 7045 + ServiceName|contains: + - 'PSEXESVC' + - 'csexec' + - 'paexec' + - 'remcom' + selection_suspicious_service: + EventID: 7045 + ServiceFileName|contains: + - '\ADMIN$\' + - 'cmd.exe /c' + - 'powershell' + condition: selection_service or selection_suspicious_service +fields: + - ServiceName + - ServiceFileName + - AccountName +falsepositives: + - Legitimate PsExec use by authorized administrators (should be documented) + - IT management tools that deploy via admin shares + - SCCM or similar endpoint management creating temporary services +level: high diff --git a/detection/sigma-rules/privilege-escalation-sudo.yml b/detection/sigma-rules/privilege-escalation-sudo.yml new file mode 100644 index 0000000..fabb030 --- /dev/null +++ b/detection/sigma-rules/privilege-escalation-sudo.yml @@ -0,0 +1,83 @@ +title: Suspicious Sudo Activity +id: c6e1a8b4-3d5f-6c0a-1e9b-7f4d2a8c3b0e +status: test +description: > + Detects unexpected sudo usage patterns on Linux systems, including sudo to root + from non-administrative users, sudoers file modifications, and sudo authentication + failures that may indicate privilege escalation attempts. +references: + - https://attack.mitre.org/techniques/T1548/003/ + - https://attack.mitre.org/techniques/T1078/003/ +author: Steven Vik +date: 2026/03/10 +tags: + - attack.privilege_escalation + - attack.t1548.003 + - attack.defense_evasion +logsource: + product: linux + service: syslog +detection: + selection_sudo_root: + process.name: 'sudo' + message|contains|all: + - 'COMMAND=' + - 'USER=root' + filter_admin_users: + user.name: + - 'soc-admin' + - 'ansible' + - 'deploy' + condition: selection_sudo_root and not filter_admin_users +fields: + - user.name + - message + - host.name + - process.pid +falsepositives: + - Developers running build tools with sudo (should use dedicated build accounts) + - Application installers requiring root access +level: medium +--- +title: Sudoers File Modification +id: d7f2b9c5-4e6a-7d1b-2f0c-8a5e3b9d4c1f +status: test +description: > + Detects modifications to the sudoers file or files in the sudoers.d directory. + Unauthorized changes to sudo configuration can grant persistent elevated access + without requiring the root password. +references: + - https://attack.mitre.org/techniques/T1548/003/ +author: Steven Vik +date: 2026/03/10 +tags: + - attack.privilege_escalation + - attack.t1548.003 + - attack.persistence +logsource: + product: linux + category: file_change +detection: + selection_sudoers: + TargetFilename|startswith: + - '/etc/sudoers' + - '/etc/sudoers.d/' + selection_visudo_bypass: + process.name|contains: + - 'echo' + - 'tee' + - 'sed' + - 'cp' + - 'mv' + process.command_line|contains: + - 'sudoers' + condition: selection_sudoers or selection_visudo_bypass +fields: + - user.name + - process.name + - process.command_line + - TargetFilename +falsepositives: + - Legitimate sudoers changes via configuration management (Ansible, Puppet, Chef) + - OS patching updating sudoers.d drop-in files +level: high diff --git a/detection/sigma-rules/suspicious-powershell.yml b/detection/sigma-rules/suspicious-powershell.yml new file mode 100644 index 0000000..ba2d409 --- /dev/null +++ b/detection/sigma-rules/suspicious-powershell.yml @@ -0,0 +1,75 @@ +title: Suspicious PowerShell Execution Patterns +id: 7e2b4a91-d3f8-4c5e-b1a9-6f0d2c8e4b3a +status: test +description: > + Detects PowerShell usage patterns commonly associated with malicious activity: + encoded commands used to obfuscate payloads, download cradles that fetch and execute + remote code, and execution policy bypass flags. These are rarely used in legitimate + administration without prior approval. +references: + - https://attack.mitre.org/techniques/T1059/001/ + - https://github.com/SigmaHQ/sigma/wiki/Rule-Creation-Guide +author: Steven Vik +date: 2026/03/10 +tags: + - attack.execution + - attack.t1059.001 + - attack.defense_evasion + - attack.t1027 +logsource: + category: process_creation + product: windows +detection: + selection_encoded: + CommandLine|contains: + - '-EncodedCommand' + - '-enc ' + - '-ec ' + - '-e ' + Image|endswith: + - '\powershell.exe' + - '\pwsh.exe' + selection_download_cradle: + CommandLine|contains: + - 'IEX' + - 'Invoke-Expression' + - 'Invoke-WebRequest' + - 'Net.WebClient' + - 'DownloadString' + - 'DownloadFile' + - 'Start-BitsTransfer' + - 'curl ' + - 'wget ' + Image|endswith: + - '\powershell.exe' + - '\pwsh.exe' + selection_bypass: + CommandLine|contains: + - '-ExecutionPolicy Bypass' + - '-ExecutionPolicy Unrestricted' + - '-ep bypass' + - 'Set-ExecutionPolicy Bypass' + - 'Set-ExecutionPolicy Unrestricted' + Image|endswith: + - '\powershell.exe' + - '\pwsh.exe' + selection_hidden: + CommandLine|contains: + - '-WindowStyle Hidden' + - '-w hidden' + - '-nop' + - '-NoProfile' + Image|endswith: + - '\powershell.exe' + - '\pwsh.exe' + condition: selection_encoded or selection_download_cradle or (selection_bypass and selection_hidden) +fields: + - User + - CommandLine + - ParentImage + - ParentCommandLine +falsepositives: + - Configuration management tools (SCCM, Ansible WinRM) may use encoded commands + - Legitimate admin scripts with -ExecutionPolicy Bypass (should be documented and whitelisted) + - Software deployment tools using download cradle patterns +level: high diff --git a/hardening/README.md b/hardening/README.md new file mode 100644 index 0000000..5d3c09d --- /dev/null +++ b/hardening/README.md @@ -0,0 +1,42 @@ +# Hardening + +Baseline security configurations for SOC lab infrastructure, inspired by CIS Benchmarks and NIST SP 800-123. + +## Approach + +**Defense-in-depth.** No single control is sufficient. Every host gets layered hardening: OS baseline, SSH lockdown, firewall rules, audit logging, and continuous validation. + +**Configuration as code.** All hardening is enforced via Ansible roles, not manual checklists. Drift is detectable and correctable by re-running the playbook. + +**Least privilege by default.** Services run as non-root. Firewalls default-deny. SSH is key-only. Sudo access is explicit and audited. + +## Structure + +``` +hardening/ +├── baselines/ # Written hardening guides (the "why" and "what") +│ ├── ssh-hardening.md +│ └── linux-baseline.md +└── ansible-roles/ # Enforceable automation (the "how") + ├── common/ # Base packages, timezone, core services + ├── ssh-hardening/ # sshd_config, fail2ban, key-only auth + └── firewall/ # UFW default-deny + allowlisted ports +``` + +## Usage + +Apply all hardening roles to a host group: + +```bash +ansible-playbook -i inventory/hosts.yml playbooks/harden.yml --limit soc-servers +``` + +Apply a single role: + +```bash +ansible-playbook -i inventory/hosts.yml playbooks/harden.yml --tags ssh +``` + +## Baselines vs. Roles + +The `baselines/` docs explain the rationale and expected state for each control. The `ansible-roles/` directories enforce that state. If you're reviewing a host manually, read the baseline. If you're deploying or remediating, run the role. diff --git a/hardening/ansible-roles/common/tasks/main.yml b/hardening/ansible-roles/common/tasks/main.yml new file mode 100644 index 0000000..1071af4 --- /dev/null +++ b/hardening/ansible-roles/common/tasks/main.yml @@ -0,0 +1,77 @@ +--- +# Role: common +# Purpose: Base configuration applied to all SOC lab hosts. +# Installs essential packages, sets timezone, enables core services. + +- name: Install base packages + ansible.builtin.apt: + name: + - curl + - wget + - git + - vim + - htop + - net-tools + - unattended-upgrades + - apt-transport-https + - ca-certificates + - gnupg + - jq + - tmux + state: present + update_cache: true + cache_valid_time: 3600 + tags: packages + +- name: Set timezone + ansible.builtin.timezone: + name: "{{ common_timezone }}" + tags: timezone + +- name: Ensure SSH service is enabled and running + ansible.builtin.systemd: + name: ssh + state: started + enabled: true + tags: ssh + +- name: Configure unattended-upgrades — periodic settings + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + owner: root + group: root + mode: "0644" + tags: updates + +- name: Configure unattended-upgrades — security only + ansible.builtin.lineinfile: + path: /etc/apt/apt.conf.d/50unattended-upgrades + regexp: '^//\s*"\${distro_id}:\${distro_codename}-security";' + line: ' "${distro_id}:${distro_codename}-security";' + state: present + tags: updates + +- name: Set hostname + ansible.builtin.hostname: + name: "{{ inventory_hostname }}" + when: common_set_hostname | default(false) + tags: hostname + +- name: Ensure /etc/hosts has localhost entry + ansible.builtin.lineinfile: + path: /etc/hosts + regexp: '^127\.0\.0\.1' + line: "127.0.0.1 localhost {{ inventory_hostname }}" + state: present + tags: hostname + +- name: Set default editor to vim + ansible.builtin.alternatives: + name: editor + path: /usr/bin/vim.basic + ignore_errors: true + tags: packages diff --git a/hardening/ansible-roles/firewall/defaults/main.yml b/hardening/ansible-roles/firewall/defaults/main.yml new file mode 100644 index 0000000..e0e1d28 --- /dev/null +++ b/hardening/ansible-roles/firewall/defaults/main.yml @@ -0,0 +1,28 @@ +--- +# Role: firewall +# Default variables for UFW firewall configuration. + +# Default policies +firewall_default_incoming: deny +firewall_default_outgoing: allow + +# SSH port — should match ssh-hardening role +firewall_ssh_port: 22 + +# Additional ports to allow (list of dicts) +# Each entry needs: port, proto (tcp/udp), and an optional comment. +# Example: +# firewall_allowed_ports: +# - { port: 443, proto: tcp, comment: "HTTPS" } +# - { port: 9090, proto: tcp, comment: "Wazuh agent" } +# - { port: 514, proto: udp, comment: "Syslog" } +firewall_allowed_ports: [] + +# Rate limiting for SSH (limits connection attempts) +firewall_ssh_rate_limit: true + +# Logging level: off, low, medium, high, full +firewall_logging: low + +# Whether to enable UFW (set to false to configure without activating) +firewall_enabled: true diff --git a/hardening/ansible-roles/firewall/handlers/main.yml b/hardening/ansible-roles/firewall/handlers/main.yml new file mode 100644 index 0000000..e911675 --- /dev/null +++ b/hardening/ansible-roles/firewall/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: Reload UFW + community.general.ufw: + state: reloaded diff --git a/hardening/ansible-roles/firewall/tasks/main.yml b/hardening/ansible-roles/firewall/tasks/main.yml new file mode 100644 index 0000000..d616b1f --- /dev/null +++ b/hardening/ansible-roles/firewall/tasks/main.yml @@ -0,0 +1,82 @@ +--- +# Role: firewall +# Purpose: Configure UFW with default-deny incoming and explicit port allowlist. + +- name: Install UFW + ansible.builtin.apt: + name: ufw + state: present + tags: firewall + +- name: Set default policy — deny incoming + community.general.ufw: + direction: incoming + default: "{{ firewall_default_incoming }}" + tags: firewall + +- name: Set default policy — allow outgoing + community.general.ufw: + direction: outgoing + default: "{{ firewall_default_outgoing }}" + tags: firewall + +- name: Allow SSH with rate limiting + community.general.ufw: + rule: limit + port: "{{ firewall_ssh_port }}" + proto: tcp + comment: "SSH (rate-limited)" + when: firewall_ssh_rate_limit + tags: firewall + +- name: Allow SSH without rate limiting + community.general.ufw: + rule: allow + port: "{{ firewall_ssh_port }}" + proto: tcp + comment: "SSH" + when: not firewall_ssh_rate_limit + tags: firewall + +- name: Allow configured ports + community.general.ufw: + rule: allow + port: "{{ item.port }}" + proto: "{{ item.proto | default('tcp') }}" + comment: "{{ item.comment | default('') }}" + loop: "{{ firewall_allowed_ports }}" + when: firewall_allowed_ports | length > 0 + tags: firewall + +- name: Set logging level + community.general.ufw: + logging: "{{ firewall_logging }}" + tags: firewall + +- name: Enable UFW + community.general.ufw: + state: enabled + when: firewall_enabled + tags: firewall + +# --- Verification --- + +- name: Check UFW status + ansible.builtin.command: ufw status verbose + register: ufw_status + changed_when: false + tags: firewall + +- name: Display UFW status + ansible.builtin.debug: + var: ufw_status.stdout_lines + tags: firewall + +- name: Assert UFW is active + ansible.builtin.assert: + that: + - "'Status: active' in ufw_status.stdout" + fail_msg: "UFW is not active" + success_msg: "UFW is active and configured" + when: firewall_enabled + tags: firewall diff --git a/hardening/ansible-roles/ssh-hardening/defaults/main.yml b/hardening/ansible-roles/ssh-hardening/defaults/main.yml new file mode 100644 index 0000000..6209177 --- /dev/null +++ b/hardening/ansible-roles/ssh-hardening/defaults/main.yml @@ -0,0 +1,52 @@ +--- +# Role: ssh-hardening +# Default variables — override in host_vars or group_vars as needed. + +# SSH daemon settings +ssh_port: 22 +ssh_permit_root_login: "no" +ssh_password_authentication: "no" +ssh_pubkey_authentication: "yes" +ssh_challenge_response_authentication: "no" +ssh_use_pam: "yes" +ssh_x11_forwarding: "no" +ssh_allow_tcp_forwarding: "no" +ssh_allow_agent_forwarding: "no" +ssh_permit_tunnel: "no" +ssh_permit_user_environment: "no" + +# Session limits +ssh_max_auth_tries: 3 +ssh_login_grace_time: 30 +ssh_max_sessions: 3 +ssh_max_startups: "10:30:60" +ssh_client_alive_interval: 300 +ssh_client_alive_count_max: 2 + +# Key algorithms +ssh_pubkey_accepted_algorithms: "ssh-ed25519,sk-ssh-ed25519@openssh.com" +ssh_host_key_algorithms: "ssh-ed25519,sk-ssh-ed25519@openssh.com" + +# Access control — set to a group or list of users +ssh_allow_groups: "ssh-users" +# ssh_allow_users: [] + +# Logging +ssh_log_level: "VERBOSE" +ssh_syslog_facility: "AUTH" + +# Banner +ssh_banner_enabled: true +ssh_banner_path: /etc/ssh/banner.txt +ssh_banner_text: | + ******************************************************************** + * This system is for authorized use only. * + * All activity is monitored and logged. * + * Unauthorized access will be investigated and reported. * + ******************************************************************** + +# Fail2ban settings +ssh_fail2ban_enabled: true +ssh_fail2ban_maxretry: 3 +ssh_fail2ban_bantime: 3600 +ssh_fail2ban_findtime: 600 diff --git a/hardening/ansible-roles/ssh-hardening/handlers/main.yml b/hardening/ansible-roles/ssh-hardening/handlers/main.yml new file mode 100644 index 0000000..1facb22 --- /dev/null +++ b/hardening/ansible-roles/ssh-hardening/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart sshd + ansible.builtin.systemd: + name: ssh + state: restarted + +- name: Restart fail2ban + ansible.builtin.systemd: + name: fail2ban + state: restarted diff --git a/hardening/ansible-roles/ssh-hardening/tasks/main.yml b/hardening/ansible-roles/ssh-hardening/tasks/main.yml new file mode 100644 index 0000000..e0b2a15 --- /dev/null +++ b/hardening/ansible-roles/ssh-hardening/tasks/main.yml @@ -0,0 +1,207 @@ +--- +# Role: ssh-hardening +# Purpose: Enforce SSH hardening baselines via sshd_config and fail2ban. + +- name: Ensure ssh-users group exists + ansible.builtin.group: + name: "{{ ssh_allow_groups }}" + state: present + when: ssh_allow_groups is defined + tags: ssh + +- name: Harden sshd_config — Port + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?Port ' + line: "Port {{ ssh_port }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — PermitRootLogin + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PermitRootLogin' + line: "PermitRootLogin {{ ssh_permit_root_login }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — PasswordAuthentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: "PasswordAuthentication {{ ssh_password_authentication }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — PubkeyAuthentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PubkeyAuthentication' + line: "PubkeyAuthentication {{ ssh_pubkey_authentication }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — ChallengeResponseAuthentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?ChallengeResponseAuthentication' + line: "ChallengeResponseAuthentication {{ ssh_challenge_response_authentication }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — MaxAuthTries + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?MaxAuthTries' + line: "MaxAuthTries {{ ssh_max_auth_tries }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — LoginGraceTime + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?LoginGraceTime' + line: "LoginGraceTime {{ ssh_login_grace_time }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — ClientAliveInterval + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?ClientAliveInterval' + line: "ClientAliveInterval {{ ssh_client_alive_interval }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — ClientAliveCountMax + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?ClientAliveCountMax' + line: "ClientAliveCountMax {{ ssh_client_alive_count_max }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — MaxSessions + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?MaxSessions' + line: "MaxSessions {{ ssh_max_sessions }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — MaxStartups + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?MaxStartups' + line: "MaxStartups {{ ssh_max_startups }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — X11Forwarding + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?X11Forwarding' + line: "X11Forwarding {{ ssh_x11_forwarding }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — AllowTcpForwarding + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?AllowTcpForwarding' + line: "AllowTcpForwarding {{ ssh_allow_tcp_forwarding }}" + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — AllowGroups + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?AllowGroups' + line: "AllowGroups {{ ssh_allow_groups }}" + when: ssh_allow_groups is defined + notify: Restart sshd + tags: ssh + +- name: Harden sshd_config — LogLevel + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?LogLevel' + line: "LogLevel {{ ssh_log_level }}" + notify: Restart sshd + tags: ssh + +- name: Deploy SSH banner + ansible.builtin.copy: + content: "{{ ssh_banner_text }}" + dest: "{{ ssh_banner_path }}" + owner: root + group: root + mode: "0644" + when: ssh_banner_enabled + tags: ssh + +- name: Enable SSH banner in sshd_config + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?Banner' + line: "Banner {{ ssh_banner_path }}" + when: ssh_banner_enabled + notify: Restart sshd + tags: ssh + +- name: Validate sshd configuration + ansible.builtin.command: sshd -t + changed_when: false + tags: ssh + +# --- Fail2ban --- + +- name: Install fail2ban + ansible.builtin.apt: + name: fail2ban + state: present + when: ssh_fail2ban_enabled + tags: fail2ban + +- name: Configure fail2ban jail for SSH + ansible.builtin.copy: + dest: /etc/fail2ban/jail.d/sshd.conf + content: | + [sshd] + enabled = true + port = {{ ssh_port }} + filter = sshd + logpath = /var/log/auth.log + maxretry = {{ ssh_fail2ban_maxretry }} + bantime = {{ ssh_fail2ban_bantime }} + findtime = {{ ssh_fail2ban_findtime }} + owner: root + group: root + mode: "0644" + when: ssh_fail2ban_enabled + notify: Restart fail2ban + tags: fail2ban + +- name: Ensure fail2ban is enabled and running + ansible.builtin.systemd: + name: fail2ban + state: started + enabled: true + when: ssh_fail2ban_enabled + tags: fail2ban + +# --- Verification --- + +- name: Verify SSH is listening on configured port + ansible.builtin.command: ss -tlnp + register: ssh_listen_check + changed_when: false + tags: ssh + +- name: Assert SSH is listening + ansible.builtin.assert: + that: + - "':' + (ssh_port | string) in ssh_listen_check.stdout" + fail_msg: "SSH is not listening on port {{ ssh_port }}" + success_msg: "SSH confirmed listening on port {{ ssh_port }}" + tags: ssh diff --git a/hardening/baselines/linux-baseline.md b/hardening/baselines/linux-baseline.md new file mode 100644 index 0000000..ca6899b --- /dev/null +++ b/hardening/baselines/linux-baseline.md @@ -0,0 +1,324 @@ +# Linux Baseline Hardening Checklist + +Minimum security configuration for all SOC lab Linux hosts. Based on CIS Benchmark Level 1 recommendations for Ubuntu/Debian. + +--- + +## 1. Package Management + +### Minimal Install +Install only what's needed. Every additional package is additional attack surface. + +```bash +# Audit installed packages +dpkg --list | wc -l + +# Remove unnecessary packages +apt purge telnet rsh-client rsh-redone-client +``` + +### Automatic Security Updates +Unattended upgrades ensure critical patches are applied without waiting for a maintenance window. + +```bash +apt install unattended-upgrades +dpkg-reconfigure -plow unattended-upgrades +``` + +Verify configuration: + +```bash +cat /etc/apt/apt.conf.d/20auto-upgrades +# APT::Periodic::Update-Package-Lists "1"; +# APT::Periodic::Unattended-Upgrade "1"; +``` + +--- + +## 2. User Management + +### Least Privilege +- No shared accounts. Every operator gets a named account. +- No direct root login. Use `sudo` with per-user grants. +- Review accounts regularly — disable what's unused. + +```bash +# List users with login shells +grep -v '/nologin\|/false' /etc/passwd + +# List sudo group members +getent group sudo + +# Lock an unused account +usermod -L -e 1 olduser +``` + +### Sudo Configuration +- Use `/etc/sudoers.d/` drop-ins (not editing sudoers directly) +- Require password for sudo (no `NOPASSWD` except for automation service accounts) +- Log all sudo usage + +```bash +# /etc/sudoers.d/analyst +analyst ALL=(ALL) ALL +Defaults:analyst log_output, log_input +``` + +### Password Policy +Even with SSH key-only auth, local passwords should have sane policies for console access: + +```bash +# /etc/login.defs +PASS_MAX_DAYS 90 +PASS_MIN_DAYS 1 +PASS_MIN_LEN 14 +PASS_WARN_AGE 14 +``` + +--- + +## 3. Filesystem + +### Mount Options +Restrict executable and setuid behavior on filesystems that don't need it: + +``` +# /etc/fstab additions +tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0 +tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0 +``` + +### File Permissions Audit +```bash +# Find world-writable files +find / -xdev -type f -perm -o+w -ls + +# Find files with no owner +find / -xdev -nouser -o -nogroup + +# Find SUID/SGID binaries +find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -ls + +# Verify critical file permissions +stat -c '%a %U:%G %n' /etc/passwd /etc/shadow /etc/group /etc/gshadow +# Expected: 644 root:root, 640 root:shadow, 644 root:root, 640 root:shadow +``` + +--- + +## 4. Network + +### Disable Unnecessary Services +```bash +# List listening services +ss -tlnp + +# Disable services not needed on this host +systemctl disable --now cups avahi-daemon +``` + +### Firewall Defaults +See `ansible-roles/firewall/` for automated enforcement. The baseline policy: + +- **Default deny** inbound +- **Default allow** outbound (restrict further in high-security zones) +- **Explicitly allow** only required services (SSH, monitoring agent, application ports) + +```bash +ufw default deny incoming +ufw default allow outgoing +ufw allow 22/tcp comment 'SSH' +ufw enable +``` + +### Kernel Network Hardening +```bash +# /etc/sysctl.d/90-network-hardening.conf + +# Disable IP forwarding (unless this host is a router) +net.ipv4.ip_forward = 0 + +# Ignore ICMP redirects +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 + +# Don't send ICMP redirects +net.ipv4.conf.all.send_redirects = 0 + +# Enable reverse path filtering (anti-spoofing) +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 + +# Ignore broadcast pings +net.ipv4.icmp_echo_ignore_broadcasts = 1 + +# Log martian packets +net.ipv4.conf.all.log_martians = 1 + +# SYN flood protection +net.ipv4.tcp_syncookies = 1 +``` + +Apply without reboot: + +```bash +sysctl --system +``` + +--- + +## 5. Logging + +### Retention +Ensure logs survive long enough for incident investigation: + +```bash +# /etc/systemd/journald.conf +[Journal] +Storage=persistent +SystemMaxUse=500M +MaxRetentionSec=90day +``` + +```bash +# /etc/logrotate.d/rsyslog — retain 90 days +/var/log/auth.log +/var/log/syslog +{ + rotate 90 + daily + compress + delaycompress + missingok + notifempty +} +``` + +### Log Forwarding +Forward logs to a central SIEM/log collector. Without forwarding, a compromised host can destroy its own logs. + +```bash +# /etc/rsyslog.d/50-forward.conf +*.* @@soc-siem-01:514 +``` + +Use TCP (`@@`) over UDP (`@`) for reliable delivery. TLS is preferred for production — see rsyslog documentation for `imtcp` with TLS configuration. + +--- + +## 6. Kernel Hardening + +Beyond network sysctl settings: + +```bash +# /etc/sysctl.d/90-kernel-hardening.conf + +# Restrict kernel pointer exposure +kernel.kptr_restrict = 2 + +# Restrict dmesg access +kernel.dmesg_restrict = 1 + +# Restrict eBPF +kernel.unprivileged_bpf_disabled = 1 + +# Restrict ptrace scope (1 = parent process only) +kernel.yama.ptrace_scope = 1 + +# Restrict kernel module loading after boot (set to 1 after all modules loaded) +# kernel.modules_disabled = 1 # WARNING: irreversible until reboot + +# ASLR enabled (should be default) +kernel.randomize_va_space = 2 +``` + +--- + +## 7. Audit Framework (auditd) + +`auditd` provides kernel-level syscall auditing — the last line of defense for detecting privilege escalation, file tampering, and unauthorized access. + +```bash +apt install auditd +systemctl enable --now auditd +``` + +### Key Audit Rules + +```bash +# /etc/audit/rules.d/soc-lab.rules + +# Monitor authentication files +-w /etc/passwd -p wa -k identity +-w /etc/group -p wa -k identity +-w /etc/shadow -p wa -k identity +-w /etc/sudoers -p wa -k sudoers +-w /etc/sudoers.d/ -p wa -k sudoers + +# Monitor SSH configuration +-w /etc/ssh/sshd_config -p wa -k sshd_config + +# Monitor cron +-w /etc/crontab -p wa -k cron +-w /etc/cron.d/ -p wa -k cron +-w /var/spool/cron/ -p wa -k cron + +# Log all sudo usage +-a always,exit -F path=/usr/bin/sudo -F perm=x -k sudo_usage + +# Log privilege escalation attempts +-a always,exit -F arch=b64 -S setuid -S setgid -k privilege_escalation + +# Log failed file access +-a always,exit -F arch=b64 -S open -S openat -F exit=-EACCES -k access_denied +-a always,exit -F arch=b64 -S open -S openat -F exit=-EPERM -k access_denied + +# Make audit config immutable (requires reboot to change) +-e 2 +``` + +Load rules: + +```bash +augenrules --load +``` + +Query audit log: + +```bash +# Recent sudo events +ausearch -k sudo_usage --start recent + +# Failed access today +ausearch -k access_denied --start today + +# Changes to /etc/passwd +ausearch -k identity -f /etc/passwd +``` + +--- + +## Validation + +Run a quick compliance check after hardening: + +```bash +# Verify no world-writable files in system dirs +find /etc /usr /var -xdev -type f -perm -o+w 2>/dev/null | wc -l +# Expected: 0 + +# Verify no accounts with empty passwords +awk -F: '($2 == "" ) { print $1 }' /etc/shadow +# Expected: no output + +# Verify SSH hardening applied +sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication' +# Expected: permitrootlogin no, passwordauthentication no, pubkeyauthentication yes + +# Verify firewall active +ufw status verbose + +# Verify auditd running +systemctl is-active auditd +``` diff --git a/hardening/baselines/ssh-hardening.md b/hardening/baselines/ssh-hardening.md new file mode 100644 index 0000000..4ea2834 --- /dev/null +++ b/hardening/baselines/ssh-hardening.md @@ -0,0 +1,182 @@ +# SSH Hardening Guide + +Secure SSH configuration for SOC lab hosts. Based on CIS Benchmark recommendations for OpenSSH Server. + +--- + +## 1. Disable Root Login + +Direct root login over SSH is unnecessary when `sudo` is configured properly. Disabling it eliminates the most targeted username in brute-force attacks. + +``` +PermitRootLogin no +``` + +## 2. Disable Password Authentication + +Password auth is vulnerable to brute-force and credential stuffing. Key-only authentication eliminates this attack surface entirely. + +``` +PasswordAuthentication no +PubkeyAuthentication yes +ChallengeResponseAuthentication no +UsePAM yes +``` + +## 3. Key-Only Authentication (Ed25519) + +Ed25519 keys are preferred — shorter, faster, and more resistant to side-channel attacks than RSA. + +Generate a key pair: + +```bash +ssh-keygen -t ed25519 -C "analyst@soc-lab" -f ~/.ssh/id_ed25519_soc +``` + +Deploy the public key: + +```bash +ssh-copy-id -i ~/.ssh/id_ed25519_soc.pub analyst@soc-web-01 +``` + +Restrict accepted key types in `sshd_config`: + +``` +PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com +HostKeyAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com +``` + +## 4. Restrict Allowed Users and Groups + +Limit SSH access to explicitly authorized accounts. This prevents service accounts or unused user accounts from being leveraged. + +``` +AllowGroups ssh-users +# Or restrict to specific users: +# AllowUsers analyst deployer +``` + +Ensure the group exists and only intended users are members: + +```bash +groupadd ssh-users +usermod -aG ssh-users analyst +``` + +## 5. Non-Default Port + +Changing the SSH port reduces noise from automated scanners. It is **not** a security control — it's an operational convenience that reduces log volume. + +``` +Port 2222 +``` + +**Trade-offs:** +- Reduces automated scan noise significantly (less log clutter, fewer fail2ban triggers) +- Adds friction for legitimate users (must specify port every time) +- Does not stop targeted attackers — port scanning reveals the service trivially +- Can break tooling that assumes port 22 +- Must be reflected in firewall rules, monitoring, and documentation + +**Recommendation:** Use a non-default port in internet-facing environments. For internal lab networks, port 22 is fine — focus hardening effort on authentication controls instead. + +## 6. Session and Authentication Limits + +Restrict brute-force windows and detect abandoned sessions: + +``` +MaxAuthTries 3 +LoginGraceTime 30 +MaxSessions 3 +MaxStartups 10:30:60 + +ClientAliveInterval 300 +ClientAliveCountMax 2 +``` + +| Setting | Purpose | +|---------|---------| +| `MaxAuthTries 3` | Lock out after 3 failed attempts per connection | +| `LoginGraceTime 30` | Close unauthenticated connections after 30 seconds | +| `MaxSessions 3` | Limit multiplexed sessions per connection | +| `MaxStartups 10:30:60` | Rate-limit unauthenticated connections | +| `ClientAliveInterval 300` | Send keepalive every 5 minutes | +| `ClientAliveCountMax 2` | Disconnect after 2 missed keepalives (10 min idle timeout) | + +## 7. Fail2ban Integration + +Fail2ban monitors auth logs and bans IPs after repeated failures. It complements `MaxAuthTries` by operating at the network level. + +```ini +# /etc/fail2ban/jail.d/sshd.conf +[sshd] +enabled = true +port = 2222 +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +bantime = 3600 +findtime = 600 +``` + +This bans an IP for 1 hour after 3 failures within 10 minutes. + +Verify bans: + +```bash +fail2ban-client status sshd +``` + +## 8. Audit Logging + +Ensure SSH events are captured for incident investigation: + +``` +LogLevel VERBOSE +SyslogFacility AUTH +``` + +`VERBOSE` logging records key fingerprints on login — critical for identifying which key was used when multiple are authorized. + +Forward SSH logs to your SIEM (see `linux-baseline.md` for log forwarding configuration). + +## 9. Additional Hardening + +Disable features that are rarely needed and expand attack surface: + +``` +X11Forwarding no +AllowTcpForwarding no +AllowAgentForwarding no +PermitTunnel no +PermitUserEnvironment no +Banner /etc/ssh/banner.txt +``` + +## 10. Testing and Validation + +After applying changes, validate before closing your current session: + +```bash +# Test config syntax (catch errors before restart) +sshd -t + +# Restart sshd +systemctl restart sshd + +# From another terminal — verify you can still log in +ssh -p 2222 analyst@soc-web-01 + +# Verify password auth is rejected +ssh -p 2222 -o PubkeyAuthentication=no analyst@soc-web-01 +# Expected: Permission denied (publickey) + +# Verify root login is rejected +ssh -p 2222 root@soc-web-01 +# Expected: Permission denied (publickey) + +# Check listening port +ss -tlnp | grep sshd +``` + +**Never close your current SSH session until you've confirmed you can open a new one.** If the config is broken and you disconnect, you'll need console access to recover. diff --git a/monitoring/README.md b/monitoring/README.md new file mode 100644 index 0000000..d78aaf4 --- /dev/null +++ b/monitoring/README.md @@ -0,0 +1,29 @@ +# Monitoring + +Prometheus alerting rules and Grafana dashboards for security-focused observability. + +## Purpose + +Monitoring in a security context isn't just uptime tracking — it's an early warning system. These configurations prioritize alerts that indicate potential compromise, data exfiltration, or infrastructure degradation that could mask an attack. + +## Stack + +- **Prometheus** — metrics collection and alerting rules +- **Grafana** — visualization dashboards +- **node_exporter** — host-level metrics (CPU, memory, disk, network) + +## Alert Design Principles + +1. **Every alert must be actionable.** If the response is "look at it and probably ignore it," the alert shouldn't exist. +2. **Severity drives response time.** Critical = immediate action. Warning = investigate within the hour. +3. **Context in the annotation.** Every alert includes what's wrong, current value, and where to start investigating. +4. **Tune before you deploy.** Thresholds are starting points — adjust based on your baseline. +5. **Security-relevant signals first.** Unusual egress, unexpected reboots, and stale backups matter more than transient CPU spikes. + +## Contents + +| File | Description | +|------|-------------| +| `prometheus-rules/node-alerts.yml` | Host-level alerts: CPU, memory, disk, network, uptime | +| `prometheus-rules/service-alerts.yml` | Service-level alerts: availability, error rates, SSL, backups | +| `dashboards/soc-overview.json` | Grafana SOC overview dashboard (importable JSON) | diff --git a/monitoring/dashboards/soc-overview.json b/monitoring/dashboards/soc-overview.json new file mode 100644 index 0000000..e5852cc --- /dev/null +++ b/monitoring/dashboards/soc-overview.json @@ -0,0 +1,174 @@ +{ + "id": null, + "uid": "soc-overview-v1", + "title": "SOC Overview", + "tags": ["soc", "security", "overview"], + "timezone": "browser", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "templating": { + "list": [ + { + "name": "DS_PROMETHEUS", + "type": "datasource", + "query": "prometheus", + "current": { "text": "Prometheus", "value": "Prometheus" } + }, + { + "name": "instance", + "type": "query", + "datasource": "${DS_PROMETHEUS}", + "query": "label_values(up, instance)", + "refresh": 2, + "multi": true, + "includeAll": true + } + ] + }, + "panels": [ + { + "id": 1, + "title": "Node Health", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "up{instance=~\"$instance\"}", + "legendFormat": "{{ instance }}" + } + ], + "fieldConfig": { + "defaults": { + "mappings": [ + { "type": "value", "options": { "0": { "text": "DOWN", "color": "red" }, "1": { "text": "UP", "color": "green" } } } + ] + } + } + }, + { + "id": 2, + "title": "Auth Failure Rate", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 6, "y": 0 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "rate(auth_failures_total{instance=~\"$instance\"}[5m])", + "legendFormat": "{{ instance }}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { "drawStyle": "line", "fillOpacity": 10 }, + "unit": "ops" + } + } + }, + { + "id": 3, + "title": "Network Egress", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "rate(node_network_transmit_bytes_total{device!~\"lo|veth.*\", instance=~\"$instance\"}[5m])", + "legendFormat": "{{ instance }} - {{ device }} (out)" + }, + { + "datasource": "${DS_PROMETHEUS}", + "expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*\", instance=~\"$instance\"}[5m])", + "legendFormat": "{{ instance }} - {{ device }} (in)" + } + ], + "fieldConfig": { + "defaults": { + "unit": "Bps", + "custom": { "drawStyle": "line", "fillOpacity": 10 } + } + } + }, + { + "id": 4, + "title": "Active Alerts", + "type": "table", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "ALERTS{alertstate=\"firing\"}", + "format": "table", + "instant": true + } + ] + }, + { + "id": 5, + "title": "Disk Usage", + "type": "bargauge", + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 12 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "(1 - node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay\", instance=~\"$instance\"} / node_filesystem_size_bytes) * 100", + "legendFormat": "{{ instance }} {{ mountpoint }}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "max": 100, + "thresholds": { + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 70 }, + { "color": "red", "value": 90 } + ] + } + } + } + }, + { + "id": 6, + "title": "CPU Usage", + "type": "timeseries", + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 12 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\", instance=~\"$instance\"}[5m])) * 100)", + "legendFormat": "{{ instance }}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "max": 100, + "custom": { "drawStyle": "line", "fillOpacity": 10 } + } + } + }, + { + "id": 7, + "title": "Memory Usage", + "type": "timeseries", + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 12 }, + "targets": [ + { + "datasource": "${DS_PROMETHEUS}", + "expr": "(1 - node_memory_MemAvailable_bytes{instance=~\"$instance\"} / node_memory_MemTotal_bytes{instance=~\"$instance\"}) * 100", + "legendFormat": "{{ instance }}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "max": 100, + "custom": { "drawStyle": "line", "fillOpacity": 10 } + } + } + } + ] +} diff --git a/monitoring/prometheus-rules/node-alerts.yml b/monitoring/prometheus-rules/node-alerts.yml new file mode 100644 index 0000000..205c827 --- /dev/null +++ b/monitoring/prometheus-rules/node-alerts.yml @@ -0,0 +1,56 @@ +groups: + - name: node-alerts + rules: + - alert: HighCPUUsage + expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90 + for: 5m + labels: + severity: warning + annotations: + summary: "High CPU usage on {{ $labels.instance }}" + description: "CPU usage has exceeded 90% for more than 5 minutes. Current value: {{ $value | printf \"%.1f\" }}%." + + - alert: HighMemoryUsage + expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory usage on {{ $labels.instance }}" + description: "Memory usage has exceeded 90% for more than 5 minutes. Current value: {{ $value | printf \"%.1f\" }}%." + + - alert: DiskSpaceLow + expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes) * 100 < 10 + for: 5m + labels: + severity: critical + annotations: + summary: "Low disk space on {{ $labels.instance }}" + description: "Filesystem {{ $labels.mountpoint }} has less than 10% free space. Current free: {{ $value | printf \"%.1f\" }}%." + + - alert: NodeDown + expr: up == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "Node {{ $labels.instance }} is down" + description: "Target {{ $labels.instance }} has been unreachable for more than 2 minutes." + + - alert: HighNetworkEgress + expr: rate(node_network_transmit_bytes_total{device!~"lo|veth.*|br.*"}[5m]) > 50 * 1024 * 1024 + for: 10m + labels: + severity: warning + annotations: + summary: "Unusual egress traffic on {{ $labels.instance }}" + description: "Interface {{ $labels.device }} is transmitting more than 50 MB/s sustained for 10 minutes. Current rate: {{ $value | humanize }}B/s. Investigate for potential data exfiltration." + + - alert: UnexpectedReboot + expr: node_time_seconds - node_boot_time_seconds < 300 + for: 0m + labels: + severity: warning + annotations: + summary: "Unexpected reboot detected on {{ $labels.instance }}" + description: "Node has been up for less than 5 minutes. Uptime: {{ $value | printf \"%.0f\" }} seconds. Check system logs for crash or unplanned restart." diff --git a/monitoring/prometheus-rules/service-alerts.yml b/monitoring/prometheus-rules/service-alerts.yml new file mode 100644 index 0000000..800ec67 --- /dev/null +++ b/monitoring/prometheus-rules/service-alerts.yml @@ -0,0 +1,52 @@ +groups: + - name: service-alerts + rules: + - alert: ServiceDown + expr: up{job!="prometheus"} == 0 + for: 3m + labels: + severity: critical + annotations: + summary: "Service {{ $labels.job }} is down on {{ $labels.instance }}" + description: "Prometheus target {{ $labels.job }} on {{ $labels.instance }} has been unreachable for more than 3 minutes." + + - alert: HighErrorRate + expr: > + ( + sum by (instance, job) (rate(http_requests_total{status=~"5.."}[5m])) + / + sum by (instance, job) (rate(http_requests_total[5m])) + ) * 100 > 5 + for: 5m + labels: + severity: warning + annotations: + summary: "High HTTP error rate on {{ $labels.instance }}" + description: "More than 5% of HTTP requests are returning 5xx errors on {{ $labels.job }}. Current error rate: {{ $value | printf \"%.1f\" }}%." + + - alert: SSLCertExpiringSoon + expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 14 + for: 1h + labels: + severity: warning + annotations: + summary: "SSL certificate expiring soon on {{ $labels.instance }}" + description: "Certificate expires in {{ $value | printf \"%.0f\" }} days. Renew before expiry to avoid service disruption." + + - alert: BackupStale + expr: time() - backup_last_success_timestamp_seconds > 90000 + for: 30m + labels: + severity: critical + annotations: + summary: "Backup is stale on {{ $labels.instance }}" + description: "Last successful backup was more than 25 hours ago. Verify backup job is running and completing successfully." + + - alert: DiskIOHigh + expr: rate(node_disk_io_time_seconds_total[5m]) > 0.9 + for: 15m + labels: + severity: warning + annotations: + summary: "High disk I/O on {{ $labels.instance }}" + description: "Disk {{ $labels.device }} has been at >90% I/O utilization for 15 minutes." diff --git a/plans/README.md b/plans/README.md new file mode 100644 index 0000000..28219e6 --- /dev/null +++ b/plans/README.md @@ -0,0 +1,103 @@ +# Roadmap + +Where this lab is headed. Each item is scoped, justified, and tracked. + +--- + +## SIEM Integration + +**What:** Deploy Wazuh or ELK stack for centralized log aggregation, correlation, and search. + +**Why:** Individual host logs don't tell the full story. A SIEM turns scattered events into correlated incidents and enables historical threat hunting. + +**Status:** Planned + +--- + +## Honeypot Deployment + +**What:** Deploy Cowrie (SSH honeypot) and evaluate T-Pot for broader protocol coverage. + +**Why:** Honeypots generate high-fidelity alerts with near-zero false positives. Any interaction is suspicious by definition — useful for detecting lateral movement and external scanning. + +**Status:** Researching + +--- + +## Threat Intelligence Feeds + +**What:** Stand up MISP for threat intel management with automated IOC ingestion from public feeds (Abuse.ch, AlienVault OTX). + +**Why:** Detection rules are only as good as the indicators behind them. Automated feed ingestion keeps signature-based detections current without manual curation. + +**Status:** Planned + +--- + +## Purple Team Exercises + +**What:** Run Atomic Red Team tests mapped directly to detection rules — validate that alerts fire when attacks execute. + +**Why:** A detection rule that has never been tested is a hypothesis, not a control. Purple teaming closes the loop between offense and defense. + +**Status:** Planned + +--- + +## Network Traffic Analysis + +**What:** Deploy Zeek and/or Suricata for passive network monitoring, PCAP analysis, and signature-based IDS. + +**Why:** Host-based telemetry misses network-level indicators — C2 beaconing patterns, DNS tunneling, lateral movement over SMB. Network visibility fills that gap. + +**Status:** Researching + +--- + +## Vulnerability Management + +**What:** Schedule regular OpenVAS/Greenbone scans against lab targets with tracked remediation. + +**Why:** You can't defend what you haven't inventoried. Scheduled scanning catches misconfigurations and missing patches before an attacker does. + +**Status:** Not Started + +--- + +## Forensics Toolkit + +**What:** Build a DFIR script library — disk imaging workflows, log collection scripts, memory analysis with Volatility. + +**Why:** Incident response speed depends on having tools and procedures ready before the incident. A cold-start forensics effort loses evidence. + +**Status:** Not Started + +--- + +## Lab Environment as Code + +**What:** Define practice targets (vulnerable VMs, attack scenarios) as Terraform and/or Vagrant configurations. + +**Why:** Reproducible environments mean repeatable testing. Tear down and rebuild a compromised target in minutes instead of hours. + +**Status:** Not Started + +--- + +## CI/CD Security Pipeline + +**What:** Integrate SAST (Semgrep, Bandit) and DAST (ZAP) scanning into a CI/CD pipeline for lab tooling and scripts. + +**Why:** Shift-left security applied to the lab's own code. Catches hardcoded secrets, injection flaws, and dependency vulnerabilities before deployment. + +**Status:** Not Started + +--- + +## Compliance Mapping + +**What:** Map all implemented controls (hardening baselines, alerting rules, access policies) to NIST 800-53 and CIS Benchmarks. + +**Why:** Controls without framework mapping are hard to communicate to auditors, hiring managers, and teams. Mapping demonstrates structured thinking, not just technical skill. + +**Status:** Planned diff --git a/playbooks/README.md b/playbooks/README.md new file mode 100644 index 0000000..e950f1b --- /dev/null +++ b/playbooks/README.md @@ -0,0 +1,38 @@ +# Incident Response Playbooks + +Structured response procedures for common security incidents, built around NIST 800-61 (Computer Security Incident Handling Guide). + +## Methodology + +Each playbook follows the NIST incident response lifecycle: + +1. **Preparation** -- tooling, access, and documentation ready before an incident +2. **Detection & Analysis** -- triage the alert, confirm scope, classify severity +3. **Containment, Eradication & Recovery** -- stop the bleeding, remove the threat, restore operations +4. **Post-Incident Activity** -- lessons learned, detection tuning, documentation + +Playbooks are not scripts. They are decision frameworks -- a practitioner uses them to stay structured under pressure, not to replace judgment. Every incident has context that a checklist cannot anticipate. + +## Template Rationale + +Each playbook includes: + +- **MITRE ATT&CK mapping** -- ties the incident type to known adversary behavior, which drives investigation priorities and detection coverage gaps +- **Trigger conditions** -- what fires this playbook (alert source, user report, automated detection) +- **Severity classification** -- consistent language across the team for escalation decisions +- **Investigation steps** -- ordered by priority, not exhaustiveness +- **Containment and remediation** -- actions with explicit decision criteria (when to isolate, when to monitor) +- **Handoff format** -- what the ticket must contain before escalation or closure + +## Playbooks + +| Playbook | MITRE Techniques | Status | +|----------|-----------------|--------| +| [Incident Triage](incident-triage.md) | General | Active | +| [Phishing Response](phishing-response.md) | T1566.001, T1566.002 | Active | +| [Malware Containment](malware-containment.md) | T1059, T1204, T1071 | Active | +| [Unauthorized Access](unauthorized-access.md) | T1078, T1110, T1021 | Active | + +## Usage + +Start with **Incident Triage** for any new alert. It determines severity and routes to the appropriate specialized playbook. Specialized playbooks assume triage is complete. diff --git a/playbooks/incident-triage.md b/playbooks/incident-triage.md new file mode 100644 index 0000000..2d2491e --- /dev/null +++ b/playbooks/incident-triage.md @@ -0,0 +1,100 @@ +# Incident Triage Playbook + +**Status:** Active +**Owner:** Steven J. Vik +**Last Updated:** 2026-03-10 +**NIST Phase:** Detection & Analysis + +## MITRE ATT&CK Mapping + +Triage is technique-agnostic -- it applies to any alert. The goal is to classify the event quickly enough to route it to the right specialized playbook, where specific technique mappings apply. + +## Trigger Conditions + +- SIEM correlation rule fires +- EDR detection alert +- User reports suspicious activity (phishing, unusual behavior, unauthorized access) +- Threat intelligence match (IOC hit on inbound/outbound traffic) +- Automated anomaly detection (baseline deviation) + +## 1. Initial Context Collection (5-10 minutes) + +Before touching any tool, answer these questions from the alert data alone: + +| Field | Source | +|-------|--------| +| Alert name and source | SIEM, EDR, email gateway, user report | +| Affected host(s) | Hostname, IP (e.g., `soc-web-01` / `10.0.0.25`) | +| Affected user(s) | Username, role, privilege level | +| Timestamp and duration | First seen, last seen, ongoing? | +| One-sentence summary | In your own words -- what happened? | + +Document this in the ticket immediately. Do not investigate further until context is recorded. + +## 2. Severity Classification + +| Level | Criteria | Response Time | +|-------|----------|---------------| +| **P1 - Critical** | Active data exfiltration, ransomware execution, compromised admin account | Immediate -- all hands | +| **P2 - High** | Confirmed compromise with limited scope, lateral movement detected | 30 minutes | +| **P3 - Medium** | Suspicious activity requiring investigation, no confirmed impact | 4 hours | +| **P4 - Low** | Informational, likely false positive, policy violation | Next business day | + +If uncertain between two levels, classify at the higher severity. Downgrade after investigation, not before. + +## 3. Investigation Steps + +### 3a. Validate the Alert + +- Is this a known false positive? Check tuning notes and previous tickets for this rule. +- Can the alert be correlated with a known change window or maintenance activity? +- Does the alert fire on a single event or a pattern? + +### 3b. Enrich with Context + +- **Host context:** Is this a server, workstation, or cloud instance? What services does it run? Check asset inventory. +- **User context:** Is this a privileged account? Service account? Has the user reported anything? +- **Network context:** Check recent connections from the host -- any known-bad IPs or unusual destinations? +- **Temporal context:** Has this host or user generated other alerts in the past 24-72 hours? + +### 3c. Check for Scope Expansion + +- Search SIEM for the same IOCs (IP, hash, domain) across all log sources +- Check if other hosts communicated with the same external destination +- Look for the same user account authenticating from multiple hosts + +## 4. Immediate Containment Decision + +These are not automatic actions. Each requires a judgment call based on severity and confidence. + +| Question | If Yes | +|----------|--------| +| Is a host actively communicating with a known C2? | Isolate the host via EDR | +| Is a compromised account still logged in? | Disable the account, kill active sessions | +| Is malware spreading laterally? | Isolate affected segment, block hash at EDR | +| Is data actively leaving the network? | Block destination at firewall, preserve traffic logs | + +**Record every action taken** with timestamp and rationale in the ticket. + +## 5. Routing + +After triage, route to the appropriate specialized playbook: + +| Indicator | Route To | +|-----------|----------| +| Phishing email, malicious link/attachment | [Phishing Response](phishing-response.md) | +| Malware execution, suspicious process, AV detection | [Malware Containment](malware-containment.md) | +| Brute force, impossible travel, privilege escalation | [Unauthorized Access](unauthorized-access.md) | +| None of the above / unclear | Continue investigation, escalate if no progress in 30 minutes | + +## 6. Handoff Format + +Before escalating or closing, the ticket must contain: + +- [ ] One-sentence summary of the alert +- [ ] Affected hosts, users, and timeframe +- [ ] Severity classification with rationale +- [ ] What was investigated and what was found +- [ ] Containment actions taken (if any) +- [ ] Recommended next steps +- [ ] Relevant log queries or artifact references diff --git a/playbooks/initial-incident-triage.md b/playbooks/initial-incident-triage.md deleted file mode 100644 index 87eaef2..0000000 --- a/playbooks/initial-incident-triage.md +++ /dev/null @@ -1,57 +0,0 @@ -# Initial Incident Triage Playbook - -Status: Draft -Owner: Steven J. Vik -Last updated: 2026-02-16 - -## 1. Trigger - -This playbook is used when a new security alert or ticket is created, for example: - -- SIEM correlation rule fires -- EDR detects suspicious activity -- User reports phishing or suspicious behavior - -## 2. Collect basic context (5–10 minutes) - -For each new alert, collect at least: - -- Alert source and name (SIEM rule, EDR detection, email report) -- Affected host(s) and user(s) -- Time range of suspicious activity -- High-level description in your own words - -Document this in the ticket before doing deeper analysis. - -## 3. Quick severity assessment - -Classify the alert as: - -- **Informational** – no clear security impact, likely noise -- **Suspicious** – requires more investigation, but impact unknown -- **Confirmed incident** – clear evidence of compromise or policy violation - -If severity is “confirmed incident”, escalate according to your IR plan. - -## 4. Immediate containment questions - -Ask: - -- Do we need to isolate a host now? -- Do we need to disable an account now? -- Do we need to block an IP, domain, or hash now? - -If the answer is “yes” and you have authority, take the action and record it in the ticket. - -## 5. Hand-off / next steps - -At the end of triage, make sure the ticket includes: - -- Summary of what triggered the alert -- What you checked -- Current severity -- Any containment actions taken -- Recommended next steps (monitor, escalate, close) - -This playbook will evolve as I learn and practice more incident response. - diff --git a/playbooks/malware-containment.md b/playbooks/malware-containment.md new file mode 100644 index 0000000..2659a77 --- /dev/null +++ b/playbooks/malware-containment.md @@ -0,0 +1,142 @@ +# Malware Containment Playbook + +**Status:** Active +**Owner:** Steven J. Vik +**Last Updated:** 2026-03-10 +**NIST Phase:** Containment, Eradication & Recovery + +## MITRE ATT&CK Mapping + +| Technique | ID | Description | +|-----------|----|-------------| +| Command and Scripting Interpreter | T1059 | Execution via PowerShell, cmd, bash, Python, etc. | +| User Execution | T1204 | User runs malicious file or link | +| Application Layer Protocol | T1071 | C2 communication over HTTP/S, DNS, etc. | + +**Related techniques** (investigation may reveal): T1053 (Scheduled Task), T1547 (Boot/Logon Autostart), T1055 (Process Injection), T1027 (Obfuscated Files) + +## Trigger Conditions + +- EDR detection: suspicious process execution, behavioral alert, or known malware signature +- AV quarantine event (especially if repeated -- suggests persistence mechanism) +- Anomalous host behavior: unexpected outbound connections, high CPU from unknown process, unusual scheduled tasks +- Threat intel IOC match on file hash or network indicator +- Escalation from [Phishing Response](phishing-response.md) after payload execution confirmed + +## Severity Classification + +| Level | Criteria | +|-------|----------| +| **P1 - Critical** | Ransomware execution, confirmed C2 communication, domain controller compromised | +| **P2 - High** | Malware executing on host, persistence established, lateral movement attempted | +| **P3 - Medium** | Malware detected and quarantined by AV/EDR, no execution confirmed | +| **P4 - Low** | Potentially unwanted program (PUP), adware, no malicious capability | + +## Containment Decision Tree + +``` +Alert fires + | + +-- Is malware actively executing? + | +-- YES -> Isolate host immediately (EDR network isolation) + | | Do NOT power off (preserves volatile evidence) + | +-- NO (quarantined by AV) -> Proceed to artifact collection + | + +-- Is there active C2 communication? + | +-- YES -> Isolate host + block C2 IP/domain at firewall + | +-- NO -> Continue investigation + | + +-- Is lateral movement detected? + | +-- YES -> Isolate affected segment, identify all touched hosts + | +-- NO -> Scope to single host, verify with network logs + | + +-- Is this a domain controller or critical server? + +-- YES -> P1 escalation, invoke full IR team + +-- NO -> Continue playbook +``` + +**Key principle:** Isolate first, investigate second. You can always reconnect a clean host. You cannot un-exfiltrate data. + +## Investigation Steps + +### 1. Host Isolation + +- Isolate via EDR (preferred -- maintains remote forensic access) +- If EDR unavailable: disable network adapter or unplug cable (document which) +- Do NOT shut down or reboot -- volatile memory contains critical evidence + +### 2. Artifact Collection + +Collect before any remediation. Order matters -- volatile data first. + +**Volatile artifacts (collect immediately):** +- Memory dump (full RAM capture via WinPMem, LiME, or EDR) +- Running processes with full command lines (`tasklist /v`, `ps auxf`) +- Active network connections (`netstat -ano`, `ss -tulnp`) +- Logged-in users and active sessions +- DNS cache (`ipconfig /displaydns`, `cat /etc/resolv.conf` + cache) + +**Non-volatile artifacts:** +- Suspicious file(s) -- copy, do not move. Record original path. +- File hashes (SHA256) for all collected samples +- Persistence mechanisms: + - Windows: Run keys, scheduled tasks, services, WMI subscriptions, startup folder + - Linux: crontabs, systemd units, `.bashrc`/`.profile`, init scripts +- Recent file modifications (last 24-72 hours) +- Browser history and download folder +- Event logs (Security, System, PowerShell, Sysmon if installed) + +### 3. Hash Lookup and Classification + +- Submit SHA256 hashes to VirusTotal +- Check against internal threat intel (MISP, previous incidents) +- Search for the hash in malware databases (MalwareBazaar, Hybrid Analysis) +- Note the malware family and known capabilities (does it exfiltrate? encrypt? persist?) + +### 4. Lateral Movement Check + +- Search SIEM for the compromised host connecting to other internal hosts (SMB, RDP, WinRM, SSH) +- Check for the same malware hash or IOC on other endpoints via EDR +- Review authentication logs for the affected user account on other systems +- Check for new or modified accounts on the compromised host + +### 5. Remediation + +**Only after artifacts are collected and scope is determined:** + +- Remove malware and persistence mechanisms +- If rootkit or deep persistence suspected: reimage the host from known-good baseline +- Reset credentials for any user account active on the compromised host +- Patch the exploitation vector if known +- Restore from backup if needed (verify backup integrity first -- some malware targets backups) + +### 6. Validation + +- Run full AV/EDR scan on remediated host +- Monitor for 48-72 hours after reconnection for reinfection indicators +- Verify no anomalous outbound connections resume + +## Evidence Preservation + +| Item | Format | Retention | +|------|--------|-----------| +| Memory dump | Raw `.mem` or `.lime` | Duration of investigation + 90 days | +| Malware sample | Password-protected ZIP (`infected`) | Indefinite (submit to internal repo) | +| Process listing | Text file with timestamps | Duration of investigation | +| Network connections | Text file with timestamps | Duration of investigation | +| Event logs | Exported `.evtx` or JSON | Duration of investigation + 90 days | +| Disk image | Raw `.dd` or `.E01` (if full forensics) | Per legal/compliance hold requirements | + +**Chain of custody:** Record who collected each artifact, when, from which host, and where it is stored. Hash all evidence files at time of collection. + +## Handoff Format + +Before closing or escalating: + +- [ ] Malware family identified (or "unknown -- sample preserved") +- [ ] All IOCs documented (hashes, C2 addresses, persistence locations) +- [ ] Scope confirmed (how many hosts, how many users) +- [ ] Containment and remediation actions with timestamps +- [ ] Evidence preserved with chain of custody +- [ ] Root cause identified (phishing, drive-by, supply chain, etc.) +- [ ] Detection gap noted (why did existing controls miss or delay detection?) diff --git a/playbooks/phishing-response.md b/playbooks/phishing-response.md new file mode 100644 index 0000000..98629e5 --- /dev/null +++ b/playbooks/phishing-response.md @@ -0,0 +1,109 @@ +# Phishing Response Playbook + +**Status:** Active +**Owner:** Steven J. Vik +**Last Updated:** 2026-03-10 +**NIST Phase:** Detection & Analysis, Containment + +## MITRE ATT&CK Mapping + +| Technique | ID | Description | +|-----------|----|-------------| +| Spearphishing Attachment | T1566.001 | Malicious file delivered via email | +| Spearphishing Link | T1566.002 | Malicious URL delivered via email | + +**Related techniques** (if payload executed): T1204 (User Execution), T1059 (Command and Scripting Interpreter) + +## Trigger Conditions + +- User reports a suspicious email (phish button, help desk ticket, direct report) +- Email gateway flags inbound message (URL reputation, attachment sandbox, header anomaly) +- EDR detects execution from email client temp directory +- Threat intel match on sender domain or embedded URL + +## Severity Classification + +| Level | Criteria | +|-------|----------| +| **P1 - Critical** | User executed payload, credentials confirmed stolen, or multiple users affected | +| **P2 - High** | User clicked link and entered credentials (unconfirmed use), or attachment opened | +| **P3 - Medium** | Phishing email delivered, no user interaction confirmed | +| **P4 - Low** | Phishing email blocked by gateway, no delivery | + +## Investigation Steps + +### 1. Email Header Analysis + +Collect from the original message (not a forward -- forwards strip headers): + +- **Envelope sender** (MAIL FROM) vs. display name -- mismatch is a strong signal +- **Received headers** -- trace the mail path, identify the originating IP +- **SPF/DKIM/DMARC results** -- did the message pass or fail authentication? +- **Reply-To** -- does it differ from the From address? +- **X-Originating-IP** -- if present, check reputation + +### 2. URL and Attachment Analysis + +**For URLs:** +- Extract all URLs from the message body and headers (don't click them) +- Check against URL reputation services (VirusTotal, URLScan.io) +- If the URL is live, submit to a sandbox for screenshot and redirect chain analysis +- Check for typosquatting on the domain (e.g., `m1crosoft.com`, `goog1e.com`) + +**For Attachments:** +- Calculate file hash (SHA256) +- Submit to sandbox (Any.Run, Joe Sandbox, or local Cuckoo instance) +- Check hash against VirusTotal and threat intel feeds +- Note file type -- `.html`, `.iso`, `.lnk`, `.one` are high-risk + +### 3. IOC Extraction + +Document all indicators for downstream blocking and hunting: + +| IOC Type | Value | Example | +|----------|-------|---------| +| Sender address | Full address | `hr-update@phishdomain.com` | +| Sender domain | Domain only | `phishdomain.com` | +| Reply-To | If different | `collector@evil.com` | +| URLs | Full URL | `https://phishdomain.com/login` | +| Attachment hash | SHA256 | `a1b2c3d4...` | +| Originating IP | Source IP | `203.0.113.50` | + +### 4. Scope Assessment -- Mailbox Search + +Search for the same message across all mailboxes: + +- Match on sender address, subject line, and/or attachment hash +- Identify all recipients and whether they opened/clicked +- Check sent folders for compromised accounts forwarding the phish internally + +### 5. Containment + +**Immediate (within 15 minutes of confirmation):** +- Block sender address and domain at the email gateway +- Quarantine all matching messages across all mailboxes +- If credential phishing: force password reset on affected users, revoke active sessions +- If payload delivered: pivot to [Malware Containment](malware-containment.md) + +**Short-term:** +- Add malicious URLs and domains to DNS sinkhole / web proxy block list +- Add file hashes to EDR block list +- Submit IOCs to threat intel platform (MISP or equivalent) + +### 6. User Communication + +- Notify affected users that the email was malicious and what action was taken +- If credentials were entered: require password reset, enable MFA if not already active +- If widespread campaign: send org-wide notification describing what to look for +- Do not blame the user. Reporting is the behavior you want to reinforce. + +## Handoff Format + +Before closing or escalating: + +- [ ] Original email preserved (`.eml` format, not screenshot) +- [ ] All IOCs documented in the ticket +- [ ] Number of recipients and confirmed interactions (opened, clicked, submitted creds) +- [ ] Containment actions with timestamps +- [ ] If payload executed: linked malware containment ticket +- [ ] Recommended detection tuning (new gateway rule, Sigma rule, etc.) diff --git a/playbooks/unauthorized-access.md b/playbooks/unauthorized-access.md new file mode 100644 index 0000000..83589ed --- /dev/null +++ b/playbooks/unauthorized-access.md @@ -0,0 +1,121 @@ +# Unauthorized Access Playbook + +**Status:** Active +**Owner:** Steven J. Vik +**Last Updated:** 2026-03-10 +**NIST Phase:** Detection & Analysis, Containment + +## MITRE ATT&CK Mapping + +| Technique | ID | Description | +|-----------|----|-------------| +| Valid Accounts | T1078 | Adversary uses legitimate credentials | +| Brute Force | T1110 | Password guessing, spraying, or credential stuffing | +| Remote Services | T1021 | Lateral movement via RDP, SSH, SMB, WinRM | + +**Related techniques:** T1098 (Account Manipulation), T1136 (Create Account), T1548 (Abuse Elevation Control Mechanism) + +## Trigger Conditions + +- **Impossible travel:** Same account authenticates from geographically distant locations within an impossible timeframe +- **Brute force alert:** Threshold exceeded for failed authentication attempts against one or more accounts +- **Privilege escalation:** User gains admin or root access outside of approved change process +- **Off-hours access:** Authentication to sensitive systems outside normal working hours +- **New device or location:** Account used from an unrecognized device, IP, or geolocation +- **Service account anomaly:** Interactive login from a service account, or service account used from unexpected host + +## Severity Classification + +| Level | Criteria | +|-------|----------| +| **P1 - Critical** | Admin/root account compromised, domain controller accessed, or active lateral movement | +| **P2 - High** | Confirmed unauthorized login to any account, or privilege escalation outside change control | +| **P3 - Medium** | Brute force in progress (no success confirmed), impossible travel on standard user | +| **P4 - Low** | Failed login spike from single source (likely automated scan), no successful auth | + +## Investigation Steps + +### 1. Account Lockdown + +**If unauthorized access is confirmed (not suspected):** +- Disable the account immediately +- Revoke all active sessions and tokens (OAuth, SSO, VPN) +- Revoke API keys associated with the account +- If service account: assess impact of disabling before acting (document the decision either way) + +**If suspected but unconfirmed:** +- Do not disable yet -- forced lockout tips off the attacker and may cause business disruption +- Force MFA re-enrollment or step-up authentication +- Monitor the account in real time + +### 2. Session Review + +- List all active and recent sessions for the account (last 72 hours) +- Identify source IPs, geolocations, device fingerprints, and user agents +- Check VPN logs: was a VPN used to appear internal? +- Check SSO/IdP logs: what applications did the account access during the suspicious session? +- Look for session token reuse from multiple IPs (session hijacking indicator) + +### 3. Authentication Log Analysis + +**Questions to answer:** +- When did the unauthorized access start? (First anomalous login, not first alert) +- Was there a brute force or spray pattern before the successful login? +- Did the attacker authenticate with the correct password on the first attempt? (Credential dump likely) +- What authentication method was used? (Password, SSO, API key, certificate) +- Was MFA bypassed, and if so, how? (MFA fatigue, SIM swap, token theft) + +**Log sources:** +- Active Directory / LDAP authentication logs +- SSO/IdP logs (Entra ID, Okta, etc.) +- VPN gateway logs +- Application-specific auth logs +- Linux: `/var/log/auth.log`, `/var/log/secure`, journal + +### 4. Lateral Movement Check + +- From the compromised account, what other systems were accessed? +- Were any new accounts created or existing accounts modified? +- Check for RDP, SSH, SMB, or WinRM connections originating from the compromised session +- Search for the source IP across all log sources -- the attacker may have used multiple accounts +- Check for Kerberos ticket anomalies (pass-the-ticket, golden ticket) if AD environment + +### 5. Credential Reset and Hardening + +- Reset password for the compromised account (generate, do not let user choose) +- Reset passwords for any account that shares credentials (yes, this happens) +- Rotate API keys and secrets associated with the account +- Enable or re-enroll MFA +- Review and remove any unauthorized MFA devices, app passwords, or recovery options +- If admin account: rotate the KRBTGT password twice (if AD compromise suspected) + +### 6. Access Audit + +- Review what the account has access to -- was any sensitive data accessed or exfiltrated? +- Check file access logs, database query logs, email forwarding rules, cloud storage downloads +- Review any changes made during the unauthorized session (group membership, permissions, configurations) +- Check for persistence: new SSH keys, scheduled tasks, forwarding rules, OAuth app grants + +## Escalation Criteria + +Escalate to full incident response if any of the following are true: + +- Admin or root account was compromised +- Lateral movement is confirmed +- Data exfiltration is suspected or confirmed +- Attacker created new accounts or modified group memberships +- The access vector is unknown (how did they get the credentials?) +- Multiple accounts are compromised (coordinated attack) + +## Handoff Format + +Before closing or escalating: + +- [ ] Compromised account(s) identified and secured +- [ ] Timeline of unauthorized access (first login through last observed activity) +- [ ] Access vector determined (brute force, credential dump, phishing, session hijack, etc.) +- [ ] Scope: what systems and data were accessed +- [ ] All containment actions with timestamps +- [ ] Lateral movement assessment (confirmed/ruled out) +- [ ] Credential reset confirmation +- [ ] Recommended hardening (MFA gaps, password policy, monitoring gaps) From aeee22e2d297fb2c93bfb475970c0b92f6a0d5e5 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:26:59 -0700 Subject: [PATCH 02/14] Add CONTRIBUTING.md --- CONTRIBUTING.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cd56e3d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Thanks for your interest in contributing! + +## Quick Fixes + +- Pick one clear thing to fix or improve +- Keep the change small and focused +- Open a PR with a clear description + +## Bigger Changes + +- Open an issue first to discuss the approach +- Include before/after context in your PR +- Keep one PR = one logical change + +## General + +- Write clear commit messages +- Be kind in discussions + +Questions? Open an issue or reach out on [LinkedIn](https://www.linkedin.com/in/stevenjvik/). From 1c549d69c263335206f8ea5f651cac0374e3ac9d Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Mon, 16 Mar 2026 02:30:30 -0700 Subject: [PATCH 03/14] Add SECURITY.md for responsible disclosure policy --- SECURITY.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8ab334d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in any SJVIK Labs project, please report it responsibly. + +**Email:** stevenjvik.work@gmail.com + +Please include: +- Description of the vulnerability +- Steps to reproduce +- Potential impact + +We will acknowledge receipt within 48 hours and aim to resolve confirmed vulnerabilities promptly. + +## Scope + +This repository contains security reference materials (playbooks, detection rules, hardening baselines, automation scripts). While the content itself describes security operations, the repository infrastructure (CI, dependencies, build pipeline) is in scope for vulnerability reports. + +## Supported Versions + +We only support the latest version on the `main` branch. \ No newline at end of file From bab313b1bff4c4c754a0da2ffa90bf18556124da Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:21:18 -0700 Subject: [PATCH 04/14] fix: add markdownlint config to relax line-length for security content Co-Authored-By: Claude Opus 4.6 --- .markdownlint.json | 4 ++++ SECURITY.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..d0225b3 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": { "line_length": 300 }, + "MD033": false +} diff --git a/SECURITY.md b/SECURITY.md index 8ab334d..67fa0e0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,4 +19,4 @@ This repository contains security reference materials (playbooks, detection rule ## Supported Versions -We only support the latest version on the `main` branch. \ No newline at end of file +We only support the latest version on the `main` branch. From 0e22d232f7cd78fb8d82b678e66aa67faa542834 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:40:50 -0700 Subject: [PATCH 05/14] fix: add markdownlint config to relax cosmetic rules Disables noisy formatting rules (blanks around headings/lists/fences, line length, inline HTML, code fence language) to get CI green. Content-focused rules remain active. Co-Authored-By: Claude Opus 4.6 --- .markdownlint.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .markdownlint.yaml diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..5a0f75c --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,8 @@ +# Relax rules for documentation-focused repo +MD013: false # Line length +MD022: false # Blanks around headings +MD031: false # Blanks around fences +MD032: false # Blanks around lists +MD033: false # Inline HTML (common in security docs) +MD040: false # Fenced code language +MD041: false # First line heading From 1275f535650c13c7d2ba43973ae528d36d25c787 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:41:47 -0700 Subject: [PATCH 06/14] fix: update markdownlint config to relax cosmetic rules The repo already had .markdownlint.json which took precedence over the .yaml added in the previous commit. Updates the JSON config to disable noisy formatting rules (blanks around headings/lists/fences, line length, bare URLs, inline HTML, code fence language). Removes redundant .yaml config. Co-Authored-By: Claude Opus 4.6 --- .markdownlint.json | 10 ++++++++-- .markdownlint.yaml | 8 -------- 2 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 .markdownlint.yaml diff --git a/.markdownlint.json b/.markdownlint.json index d0225b3..f364161 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,4 +1,10 @@ { - "MD013": { "line_length": 300 }, - "MD033": false + "MD013": false, + "MD022": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD034": false, + "MD040": false, + "MD041": false } diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index 5a0f75c..0000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Relax rules for documentation-focused repo -MD013: false # Line length -MD022: false # Blanks around headings -MD031: false # Blanks around fences -MD032: false # Blanks around lists -MD033: false # Inline HTML (common in security docs) -MD040: false # Fenced code language -MD041: false # First line heading From a8c037dbd0dadbee059c955b41a0a1117e6db975 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:07:43 -0700 Subject: [PATCH 07/14] docs: update README to reflect current state Co-Authored-By: Claude Opus 4.6 --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ec80fdb..583ee9c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,60 @@ -# SJV SOC Lab +# sec-lab -I am rebuilding my cybersecurity lab and portfolio from scratch in 2026. +A hands-on security operations lab. IR playbooks, Sigma detection rules, hardening baselines, monitoring configs, and automation scripts, all built around real home lab infrastructure. -This repo will collect: -- Simple incident response playbooks -- Small PowerShell and Python tools for log triage -- Notes from my home lab and learning experiments +This isn't a production SOC. It's a working portfolio of security engineering practices, grounded in NIST, MITRE ATT&CK, and CIS frameworks. -Nothing here is production-ready. It is a playground for me to learn and show progress. +## What's in here + +### Playbooks (`playbooks/`) + +Four incident response playbooks following the NIST 800-61 lifecycle. Each one maps to MITRE ATT&CK techniques and includes triage criteria, investigation steps, and containment actions. + +- Incident Triage (general entry point) +- Phishing Response (T1566) +- Malware Containment (T1059, T1204, T1071) +- Unauthorized Access (T1078, T1110, T1021) + +### Detection (`detection/`) + +Sigma-format detection rules. Vendor-neutral, version-controlled, testable against log samples before deployment. + +- Brute force authentication +- Lateral movement over SMB +- Privilege escalation via sudo +- Suspicious PowerShell execution + +Also includes alert correlation patterns and threshold vs. anomaly detection docs. + +### Hardening (`hardening/`) + +CIS/NIST-inspired baselines with Ansible roles that enforce them. Not just documentation. The roles are runnable. + +- SSH hardening (key-only auth, fail2ban, sshd config) +- Firewall (UFW default-deny, allowlisted ports) +- Common baseline (packages, timezone, core services) +- Written guides explaining the rationale for each control + +### Monitoring (`monitoring/`) + +Prometheus alerting rules and a Grafana SOC overview dashboard. Alerts are security-focused: unusual egress, unexpected reboots, stale backups. Every alert has context annotations and severity classifications. + +### Automation (`automation/`) + +Python and Bash scripts for SOC workflows. Zero external dependencies. + +- `log-parser.py` parses auth.log for failed/successful logins, summarizes by IP +- `backup-validator.sh` checks backup recency, size, and checksum integrity +- `ioc-checker.py` triages IOCs against a local blocklist + +### Roadmap (`plans/`) + +Tracked next steps: SIEM integration (Wazuh/ELK), honeypot deployment, threat intel feeds via MISP, purple team exercises with Atomic Red Team, network traffic analysis, vulnerability scanning, and forensics toolkit buildout. + +## CI + +A GitHub Actions workflow runs markdownlint on PRs and pushes to main. + +## License + +[MIT](LICENSE) From 6a4a0ca4add29e266cb878ebc4de615e886b76d7 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:11:33 -0700 Subject: [PATCH 08/14] chore: add .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d1239b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +*.swp +*~ +.idea/ +.vscode/ +__pycache__/ +*.pyc +.env +*.log From 215793b0b13b327f01c5aafacc92874e0e91c9e3 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:33:12 -0700 Subject: [PATCH 09/14] docs: update README with current lab stack and structure Replaces placeholder text with accurate description of Wazuh SIEM, 7 monitored endpoints, MITRE ATT&CK mapping, and repo structure. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 73 +++++++++++++++---------------------------------------- 1 file changed, 19 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 583ee9c..6e151d3 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,25 @@ -# sec-lab +# SJV SOC Lab -A hands-on security operations lab. IR playbooks, Sigma detection rules, hardening baselines, monitoring configs, and automation scripts, all built around real home lab infrastructure. +Cybersecurity lab and portfolio built on a self-hosted Proxmox cluster. Running Wazuh SIEM with 7 monitored endpoints, custom detection rules mapped to MITRE ATT&CK, and incident response playbooks. -This isn't a production SOC. It's a working portfolio of security engineering practices, grounded in NIST, MITRE ATT&CK, and CIS frameworks. +## What's Here -## What's in here +- **[siem/](siem/)** - Wazuh SIEM deployment, custom rules, agent enrollment, Sigma-to-Wazuh conversion +- **[playbooks/](playbooks/)** - Incident response playbooks for common scenarios +- **[detection/](detection/)** - Sigma detection rules and alert logic +- **[hardening/](hardening/)** - OS and service hardening baselines +- **[monitoring/](monitoring/)** - Infrastructure monitoring notes +- **[automation/](automation/)** - Scripts for log triage and response +- **[plans/](plans/)** - Roadmap for lab expansion -### Playbooks (`playbooks/`) +## Lab Stack -Four incident response playbooks following the NIST 800-61 lifecycle. Each one maps to MITRE ATT&CK techniques and includes triage criteria, investigation steps, and containment actions. +| Component | Details | +|-----------|---------| +| SIEM | Wazuh 4.9.2 all-in-one (manager + indexer + dashboard) | +| Endpoints | 7 Debian 12 LXC containers on Proxmox | +| Detection | Built-in rules + custom rules with MITRE ATT&CK mapping | +| Alerting | Email via msmtp/Gmail for high-severity events | +| Monitoring | Grafana + Prometheus (infra), Wazuh (security) | -- Incident Triage (general entry point) -- Phishing Response (T1566) -- Malware Containment (T1059, T1204, T1071) -- Unauthorized Access (T1078, T1110, T1021) - -### Detection (`detection/`) - -Sigma-format detection rules. Vendor-neutral, version-controlled, testable against log samples before deployment. - -- Brute force authentication -- Lateral movement over SMB -- Privilege escalation via sudo -- Suspicious PowerShell execution - -Also includes alert correlation patterns and threshold vs. anomaly detection docs. - -### Hardening (`hardening/`) - -CIS/NIST-inspired baselines with Ansible roles that enforce them. Not just documentation. The roles are runnable. - -- SSH hardening (key-only auth, fail2ban, sshd config) -- Firewall (UFW default-deny, allowlisted ports) -- Common baseline (packages, timezone, core services) -- Written guides explaining the rationale for each control - -### Monitoring (`monitoring/`) - -Prometheus alerting rules and a Grafana SOC overview dashboard. Alerts are security-focused: unusual egress, unexpected reboots, stale backups. Every alert has context annotations and severity classifications. - -### Automation (`automation/`) - -Python and Bash scripts for SOC workflows. Zero external dependencies. - -- `log-parser.py` parses auth.log for failed/successful logins, summarizes by IP -- `backup-validator.sh` checks backup recency, size, and checksum integrity -- `ioc-checker.py` triages IOCs against a local blocklist - -### Roadmap (`plans/`) - -Tracked next steps: SIEM integration (Wazuh/ELK), honeypot deployment, threat intel feeds via MISP, purple team exercises with Atomic Red Team, network traffic analysis, vulnerability scanning, and forensics toolkit buildout. - -## CI - -A GitHub Actions workflow runs markdownlint on PRs and pushes to main. - -## License - -[MIT](LICENSE) +This is a working lab, not a production environment. Everything here is built for learning and demonstrating real skills. From 1234fa27843a90e5d002535b245608e714092bd0 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:33:45 -0700 Subject: [PATCH 10/14] feat: add Wazuh SIEM deployment and mark SIEM phase complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add siem/ directory with Wazuh 4.9.2 deployment notes, dashboards, Sigma-to-Wazuh conversion scripts, and custom rules - Update plans/README.md — SIEM status: Complete (7 agents, email alerting) Co-Authored-By: Claude Sonnet 4.6 --- plans/README.md | 2 +- siem/README.md | 85 ++++++++++++++++++++++ siem/dashboards/README.md | 24 +++++++ siem/sigma-to-wazuh/README.md | 34 +++++++++ siem/wazuh/active-response/block-ip.sh | 39 ++++++++++ siem/wazuh/agent-enrollment.md | 58 +++++++++++++++ siem/wazuh/deployment-guide.md | 98 ++++++++++++++++++++++++++ siem/wazuh/local_rules.xml | 61 ++++++++++++++++ 8 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 siem/README.md create mode 100644 siem/dashboards/README.md create mode 100644 siem/sigma-to-wazuh/README.md create mode 100644 siem/wazuh/active-response/block-ip.sh create mode 100644 siem/wazuh/agent-enrollment.md create mode 100644 siem/wazuh/deployment-guide.md create mode 100644 siem/wazuh/local_rules.xml diff --git a/plans/README.md b/plans/README.md index 28219e6..4e1bc43 100644 --- a/plans/README.md +++ b/plans/README.md @@ -10,7 +10,7 @@ Where this lab is headed. Each item is scoped, justified, and tracked. **Why:** Individual host logs don't tell the full story. A SIEM turns scattered events into correlated incidents and enables historical threat hunting. -**Status:** Planned +**Status:** Complete (Wazuh 4.9.2, 7 agents, custom rules, email alerting) --- diff --git a/siem/README.md b/siem/README.md new file mode 100644 index 0000000..aca65ca --- /dev/null +++ b/siem/README.md @@ -0,0 +1,85 @@ +# SIEM Integration + +Wazuh 4.9.2 all-in-one deployment running on a Proxmox LXC, collecting events from 7 endpoints across the lab. + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Wazuh Manager │ +│ LXC 107 · 192.168.10.37 · wazuh.lan │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ Manager │ │ Indexer │ │ Dashboard │ │ +│ │ (1514) │ │ (9200) │ │ (443/HTTPS) │ │ +│ └──────────┘ └──────────┘ └──────────────────┘ │ +└─────────────────┬───────────────────────────────────┘ + │ Agent enrollment (1515) + ┌───────────┼───────────┬──────────────┐ + │ │ │ │ + ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ ┌────┴────┐ + │nx-web │ │monitor│ │adguard│ │ + 4 more│ + │ -01 │ │ │ │ │ │ agents │ + └───────┘ └───────┘ └───────┘ └─────────┘ +``` + +## What's Deployed + +| Component | Version | Location | +|-----------|---------|----------| +| Wazuh Manager | 4.9.2 | LXC 107 (all-in-one) | +| Wazuh Indexer | 4.9.2 | Same LXC (OpenSearch) | +| Wazuh Dashboard | 4.9.2 | https://wazuh.lan | +| Agents | 4.9.2 | 7 LXC endpoints | + +## Agents + +| Agent | VMID | Host | Priority | +|-------|------|------|----------| +| nx-web-01 | 101 | Web services | High | +| monitor | 105 | Grafana/Prometheus | High | +| syncthing | 102 | Vault data sync | Medium | +| adguard | 100 | DNS | Medium | +| traefik | 104 | Reverse proxy | Medium | +| uptime-kuma | 108 | Availability monitoring | Low | +| 5etools | 106 | D&D reference | Low | + +## Detection Coverage + +Out of the box: +- File Integrity Monitoring (FIM) on /etc, /root, /home +- Rootkit detection +- SSH brute force detection +- Log analysis (syslog, auth.log, dpkg.log) +- Vulnerability detection (CVE scanning) +- CIS Debian 12 benchmark (SCA) + +Custom rules (see `wazuh/local_rules.xml`): +- Sudo privilege escalation (100100-100101) +- Sudoers file modification via FIM (100110) +- Internal SSH lateral movement (100120) +- Critical service state changes (100130) + +## Directory Structure + +``` +siem/ +├── README.md # This file +├── wazuh/ +│ ├── deployment-guide.md # Step-by-step LXC + Wazuh install +│ ├── agent-enrollment.md # Agent deployment across fleet +│ ├── local_rules.xml # Custom detection rules +│ └── active-response/ +│ └── block-ip.sh # Active response script +├── sigma-to-wazuh/ +│ ├── README.md # Conversion methodology +│ └── converted-rules/ # Wazuh rules from Sigma +└── dashboards/ + └── README.md # Dashboard customization notes +``` + +## Related + +- [Detection Rules](../detection/) - Sigma rules that map to the custom Wazuh rules +- [Monitoring](../monitoring/) - Prometheus/Grafana infra monitoring (separate from security monitoring) +- [Playbooks](../playbooks/) - IR procedures that reference SIEM alerts diff --git a/siem/dashboards/README.md b/siem/dashboards/README.md new file mode 100644 index 0000000..9ba3a78 --- /dev/null +++ b/siem/dashboards/README.md @@ -0,0 +1,24 @@ +# Dashboard Customization + +The Wazuh dashboard is an OpenSearch Dashboards fork accessible at `https://wazuh.lan`. + +## Default Views + +Out of the box, the dashboard includes: +- **Security Events**: Aggregated alerts by severity, agent, rule group +- **Integrity Monitoring**: FIM events across all agents +- **Vulnerability Detection**: CVE scan results per agent +- **SCA (Security Configuration Assessment)**: CIS benchmark compliance scores +- **MITRE ATT&CK**: Alert mapping to the ATT&CK framework + +## Custom Dashboards (Planned) + +- **Lab Overview**: Single-pane view of all 7 agents with alert counts by severity +- **Lateral Movement Tracker**: SSH logins between internal hosts over time +- **FIM Heatmap**: File change frequency across endpoints + +## Access + +- URL: `https://wazuh.lan` +- Default admin credentials are generated during install +- Routed through Traefik with `insecureSkipVerify` (self-signed cert) diff --git a/siem/sigma-to-wazuh/README.md b/siem/sigma-to-wazuh/README.md new file mode 100644 index 0000000..7dc51b5 --- /dev/null +++ b/siem/sigma-to-wazuh/README.md @@ -0,0 +1,34 @@ +# Sigma to Wazuh Conversion + +Converting Sigma detection rules to Wazuh format. + +## Approach + +Sigma rules in `detection/sigma-rules/` define detection logic in a vendor-neutral format. Wazuh uses its own XML rule format with different matching semantics. + +The conversion is manual for now. Each Sigma rule maps to one or more Wazuh `` entries in `local_rules.xml`. + +## Mapping Table + +| Sigma Rule | Wazuh Rule ID | Coverage | +|------------|---------------|----------| +| `brute-force-auth.yml` | Built-in (5710-5712) | Full - Wazuh has native SSH brute force detection | +| `privilege-escalation-sudo.yml` | 100100, 100101, 100110 | Full - sudo exec, auth failure, sudoers FIM | +| `lateral-movement-smb.yml` | 100120 (SSH variant) | Partial - SSH lateral movement only, no SMB in lab | +| `suspicious-powershell.yml` | N/A | Not applicable - no Windows endpoints in lab | + +## Conversion Notes + +Sigma's `logsource` maps to Wazuh's `` (parent rule) and `` (pattern). Key differences: + +- Sigma uses field-based matching. Wazuh uses regex on the full log line or decoded fields. +- Sigma supports `|contains|all` for AND matching. Wazuh uses multiple `` tags. +- Sigma `filter` sections become Wazuh `` + negated match or separate exclusion rules. +- MITRE ATT&CK mapping is supported natively in both formats. + +## Future + +If the rule count grows, evaluate `sigma-cli` with the Wazuh backend for automated conversion: +```bash +sigma convert -t wazuh -p wazuh-linux detection/sigma-rules/*.yml +``` diff --git a/siem/wazuh/active-response/block-ip.sh b/siem/wazuh/active-response/block-ip.sh new file mode 100644 index 0000000..1aac26e --- /dev/null +++ b/siem/wazuh/active-response/block-ip.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Active Response: Block IP via iptables +# Triggered by Wazuh when a rule with active-response fires +# +# Usage: Configured in ossec.conf as: +# +# block-ip +# local +# 100101 +# 3600 +# + +ACTION=$1 +USER=$2 +IP=$3 + +LOG="/var/ossec/logs/active-responses.log" + +if [ -z "$IP" ]; then + echo "$(date) - No IP provided, exiting" >> "$LOG" + exit 1 +fi + +case "$ACTION" in + add) + iptables -I INPUT -s "$IP" -j DROP + echo "$(date) - Blocked $IP" >> "$LOG" + ;; + delete) + iptables -D INPUT -s "$IP" -j DROP + echo "$(date) - Unblocked $IP" >> "$LOG" + ;; + *) + echo "$(date) - Unknown action: $ACTION" >> "$LOG" + exit 1 + ;; +esac + +exit 0 diff --git a/siem/wazuh/agent-enrollment.md b/siem/wazuh/agent-enrollment.md new file mode 100644 index 0000000..7d74b92 --- /dev/null +++ b/siem/wazuh/agent-enrollment.md @@ -0,0 +1,58 @@ +# Agent Enrollment + +How to deploy Wazuh agents across the LXC fleet and enroll them with the manager. + +## Agent Install (Debian 12) + +```bash +# Add Wazuh repo +curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --dearmor -o /usr/share/keyrings/wazuh.gpg +echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \ + > /etc/apt/sources.list.d/wazuh.list + +# Install agent pinned to manager version +apt-get update +WAZUH_MANAGER="192.168.10.37" apt-get install -y wazuh-agent=4.9.2-1 + +# Enable and start +systemctl daemon-reload +systemctl enable --now wazuh-agent +``` + +**Important:** The agent version must match the manager version. Installing a newer agent against an older manager will fail with "Agent version must be lower or equal to manager version." + +## Verify Enrollment + +From the manager: +```bash +# List all agents +/var/ossec/bin/manage_agents -l + +# Check specific agent status +/var/ossec/bin/agent_control -i 001 +``` + +From the agent: +```bash +# Check agent logs for successful connection +tail -20 /var/ossec/logs/ossec.log +``` + +## Batch Deployment + +For deploying across multiple LXCs, prerequisites first: +```bash +# Some minimal LXCs don't have curl or gnupg +apt-get install -y curl gnupg +``` + +Then run the install block above. The `WAZUH_MANAGER` environment variable sets the manager IP in `ossec.conf` during install, so no manual config editing is needed. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Agent version must be lower or equal" | Agent newer than manager | Pin agent version: `apt-get install wazuh-agent=4.9.2-1` | +| "Invalid server address: MANAGER_IP" | Config has placeholder | `sed -i 's/MANAGER_IP/192.168.10.37/' /var/ossec/etc/ossec.conf` | +| "No such tag 'users'" | Config from newer version | Purge and reinstall with correct version | +| Agent shows "Never connected" | Agent service not running | `systemctl restart wazuh-agent` | diff --git a/siem/wazuh/deployment-guide.md b/siem/wazuh/deployment-guide.md new file mode 100644 index 0000000..ae56601 --- /dev/null +++ b/siem/wazuh/deployment-guide.md @@ -0,0 +1,98 @@ +# Wazuh Deployment Guide + +How the Wazuh all-in-one (manager + indexer + dashboard) was deployed on a Proxmox LXC. + +## Prerequisites + +- Proxmox VE 8.x with available resources (2 CPU, 4GB RAM, 32GB disk) +- Debian 12 LXC template +- Network connectivity between all endpoints + +## Step 1: Create the LXC + +```bash +pct create 107 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \ + --hostname wazuh \ + --cores 2 \ + --memory 4096 \ + --swap 512 \ + --rootfs local-lvm:32 \ + --net0 name=eth0,bridge=vmbr0,ip=192.168.10.37/24,gw=192.168.10.1 \ + --unprivileged 1 \ + --features nesting=1 \ + --start 1 +``` + +## Step 2: Prepare the OS + +```bash +apt-get update && apt-get install -y curl sudo openssh-server +systemctl enable --now sshd +``` + +## Step 3: Install Wazuh All-in-One + +```bash +curl -sO https://packages.wazuh.com/4.9/wazuh-install.sh +bash wazuh-install.sh -a +``` + +This installs: +- **Wazuh Manager** (event collection, rule engine, active response) on port 1514 +- **Wazuh Indexer** (OpenSearch-based log storage) on port 9200 +- **Wazuh Dashboard** (web UI) on port 443 + +The installer outputs the admin password. Save it. + +## Step 4: Post-Install Configuration + +### DNS (AdGuard) +Add rewrite: `wazuh.lan` -> `192.168.10.37` + +### Reverse Proxy (Traefik) +Add router and service for `wazuh.lan` pointing to `https://192.168.10.37:443`. +Requires `insecureSkipVerify` since Wazuh uses self-signed TLS. + +### SSH Hardening +```bash +apt-get install -y fail2ban unattended-upgrades +sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +systemctl restart sshd +``` + +### Monitoring (Uptime Kuma) +Add HTTP monitor for `https://192.168.10.37:443` with TLS verification disabled. + +### Email Alerting +```bash +apt-get install -y msmtp msmtp-mta +``` + +Configure `/etc/msmtprc` with SMTP credentials. Wazuh uses `/usr/sbin/sendmail` which msmtp-mta provides. + +Enable in `/var/ossec/etc/ossec.conf`: +```xml +yes +localhost +alerts@yourdomain.com +you@yourdomain.com +``` + +## Step 5: Verify + +```bash +# Dashboard responds +curl -sk https://wazuh.lan + +# Manager is running +systemctl is-active wazuh-manager + +# Check enrolled agents +/var/ossec/bin/agent_control -l +``` + +## Resource Usage + +After full deployment with 7 agents: +- Manager LXC: ~1.5GB RAM, minimal CPU at idle +- Each agent: ~50MB RAM on the endpoint diff --git a/siem/wazuh/local_rules.xml b/siem/wazuh/local_rules.xml new file mode 100644 index 0000000..ecf57c0 --- /dev/null +++ b/siem/wazuh/local_rules.xml @@ -0,0 +1,61 @@ + + + + + + 5402 + COMMAND= + USER=root + Sudo to root executed - privilege escalation check + + T1548.003 + + privilege_escalation, + + + + 5401 + 3 incorrect password attempts + Sudo authentication failure - possible privilege escalation attempt + + T1548.003 + + privilege_escalation,authentication_failure, + + + + + + + 550,553,554 + /etc/sudoers + Sudoers file modified - high priority security event + + T1548.003 + + privilege_escalation,file_integrity, + + + + + + + 5715 + 192.168.10.0/24 + SSH login from internal network - potential lateral movement + + T1021.004 + + lateral_movement, + + + + + + + 5104 + wazuh-manager|wazuh-indexer|wazuh-dashboard + Critical Wazuh service state changed + service_availability, + + From 8a6b592ffe2878fe7ba8862854845deb11ed212a Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:51:41 -0700 Subject: [PATCH 11/14] =?UTF-8?q?docs:=20update=20README=20=E2=80=94=20rem?= =?UTF-8?q?ove=20disclaimers,=20reflect=20NOC-grade=20operation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e151d3..c9ace50 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SJV SOC Lab -Cybersecurity lab and portfolio built on a self-hosted Proxmox cluster. Running Wazuh SIEM with 7 monitored endpoints, custom detection rules mapped to MITRE ATT&CK, and incident response playbooks. +Cybersecurity lab and portfolio built on a self-hosted Proxmox cluster. Running Wazuh SIEM with monitored endpoints, custom detection rules mapped to MITRE ATT&CK, and incident response playbooks. ## What's Here @@ -17,9 +17,9 @@ Cybersecurity lab and portfolio built on a self-hosted Proxmox cluster. Running | Component | Details | |-----------|---------| | SIEM | Wazuh 4.9.2 all-in-one (manager + indexer + dashboard) | -| Endpoints | 7 Debian 12 LXC containers on Proxmox | +| Endpoints | Debian 12 LXC containers on 3-node Proxmox HA cluster | | Detection | Built-in rules + custom rules with MITRE ATT&CK mapping | | Alerting | Email via msmtp/Gmail for high-severity events | | Monitoring | Grafana + Prometheus (infra), Wazuh (security) | -This is a working lab, not a production environment. Everything here is built for learning and demonstrating real skills. +Everything here comes from running production-grade infrastructure — real configs, real detections, real lessons learned. From af98af2cb04bafbf795369432636e5193a5e98e8 Mon Sep 17 00:00:00 2001 From: "Steven J. Vik" <154385603+stevenjvik@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:54:19 -0700 Subject: [PATCH 12/14] chore: add dependabot weekly update schedule --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..35b4450 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: ["dependencies"] From 771a1df576bd76eb3b67f7780a70bd3faa6df40b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:02:44 -0700 Subject: [PATCH 13/14] chore(deps): bump actions/checkout from 4 to 6 (#3) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dbcbacc..67dd1a0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ jobs: markdownlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: DavidAnson/markdownlint-cli2-action@v16 with: globs: "**/*.md" From 94b4bae9b3dde788a7acf625cbdd81b9fc0d02e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:54:40 +0000 Subject: [PATCH 14/14] chore(deps): bump DavidAnson/markdownlint-cli2-action from 16 to 22 Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 16 to 22. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v16...v22) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-version: '22' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 67dd1a0..6f7a0e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,6 +5,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: DavidAnson/markdownlint-cli2-action@v16 + - uses: DavidAnson/markdownlint-cli2-action@v22 with: globs: "**/*.md"