Project rename. This module was published as
PSGuerrillathrough version 2.46.1 and is nowGuerrilla, effective v2.46.3 (2026-07-08). The repository and the PowerShell Gallery package are bothGuerrilla; the oldPSGuerrillapackage remains on the Gallery frozen at its last version. Existing installs migrate automatically (per-user data and safehouse credentials carry forward transparently, see v2.46.3 below). Changelog entries for versions published asPSGuerrillaare left exactly as they were at the time; history is not rewritten.
- Generated reports now localize into 13 languages. Every HTML report honors a
-ReportLanguagecode onInvoke-ADAudit,Invoke-EntraAudit,Invoke-GWSAudit,Invoke-Campaign, and theExport-TechnicalReport/Export-ExecutiveSummary/Export-RemediationPlaybook/Export-BudgetJustificationcmdlets (also settable asoutput.reportLanguagein config). The report shell and all 643 checks' static content — name, description, recommended value, and remediation steps — render in Spanish, French, German, Italian, Hebrew, Portuguese, Dutch, Danish, Russian, Simplified Chinese, Japanese, Korean, and Hindi. The<html lang>/dirattributes follow the selected language, right-to-left languages (Hebrew) mirror the layout, and each translated string carries per-key provenance. Live collected evidence stays as collected — only the check definitions are translated; any string a language is missing falls back to English. A localization gate (154 checks) fails the build if a shipped language is missing a check, shell key, category label, or format placeholder, and it proves its own scanner can fail with a poison self-test. - All 13 translations ship as machine-draft pending native review. Test reports (
-TestMode) render in the selected language too.
- This supersedes the "generated reports remain English only" note from v2.52.0: report output is now localized for the 13 languages above. The desktop GUI's 41-language coverage is unchanged; the remaining GUI languages are not yet available for report output.
- Two Google Workspace attack-path checks.
DRIVE-018(Shared Drive External-Sharing Exposure) enumerates every shared drive in the tenant and flags each whose external-sharing restriction (domainUsersOnly) is not enforced — the drive-level gate that permits a shared drive's contents to be shared outside the organization.GTRADE-007(Admin Role Granted Through a Group) surfaces admin roles assigned to a security group rather than a user: every direct or nested member inherits the role, and anyone who can edit the group's membership can grant it to themselves. It is the Google Workspace analogue of an Active Directory nested-group-to-Tier-0 path; broad-privilege grantsFAIL, narrow onesWARNfor the district's review. - Five more K12 candidate-baseline checks (
GWS-K12-011throughGWS-K12-015): student Gmail auto-forwarding, Google Takeout data export, legacy IMAP/POP authentication for students, Google Vault export/retention/eDiscovery privilege sprawl, and audit-logging license coverage. Where a control's state is not exposed by any readable policy surface (Takeout, per-user audit-log SKU), the check reports Not Assessed with a manual-review direction rather than assuming a value it cannot read. The K12 baseline remains a Guerrilla-authored candidate community baseline, openly published for comment, not a consensus standard. - Google Workspace coverage is now 175 checks; the total is 643, each validated by a golden-fixture test (1,854 fixtures).
- Service-account key staging is now owner-only. When a scan resolves a Google service-account private key from the vault, it must stage it to a temp file for the collector, which only accepts a key by path. That file is now created empty, locked to the current user (mode
0600on Unix; an inheritance-protected single-user ACL on Windows), and only then written — so the plaintext key is never world-readable, not even for the instant between creation and the permission change. The name now uses fullGetRandomFileName()entropy instead of a truncated GUID slice. Routed all five staging sites (Invoke-GWSAudit,Invoke-Campaign,Test-Safehouse) through one helper,New-GuerrillaSecretFile.
- The desktop GUI now speaks 41 languages, four of them right-to-left. Every user-visible string in Show-Guerrilla comes from locale catalogs in
Data/Locales/(English is the source; each of the other 40 carries all 171 keys with per-key translation provenance). A header selector switches the whole window live, the choice persists in config, and first launch follows the OS display language when a matching catalog exists. Right-to-left languages (Arabic, Persian, Hebrew, Urdu) mirror the entire layout via a catalog-declared direction flag, while the run log and source inspector stay left-to-right because cmdlet output is English regardless of UI language. Adding a language is adding one catalog file: the selector discovers them at runtime, and codes may be script-qualified (Traditional Chinese ships aszh-Hantalongside Simplifiedzh). A localization gate (259 checks) fails the build if any string is missing from English, a shipped language is incomplete, a format placeholder is dropped or invented, or a declared text direction is invalid, and it proves its own scanner can fail with a poison self-test. The 40 translations ship as machine-draft pending native review; the per-key provenance tracks that. Generated reports remain English only; this release localizes the application interface, not the HTML/PDF output. - Ten more GUI languages, taking the toggle to 41. Persian (RTL), Swahili, Amharic, Malay, Filipino, Romanian, Greek, Czech, Hungarian, and Traditional Chinese. Coverage now spans a Middle Eastern RTL, two major African languages (one in Ge'ez script), Southeast Asian (Malay distinct from Indonesian, Filipino), four more European languages including Greek's own script, and the Traditional/Simplified Chinese split (Taiwan conventions, verified distinct from the Simplified catalog). Persian is the fourth right-to-left language and reused the existing mechanism with no code changes. Traditional Chinese is the first catalog with a script-qualified BCP-47 code (
zh-Hant); the loader, selector, config persistence, and gate are all string-based, so it flows through unchanged. The localization gate now covers all 41 languages (259 checks). - Twenty more GUI languages, taking the toggle to 31. French, Italian, Hebrew (RTL), nine Indian languages (Bengali, Tamil, Telugu, Marathi, Gujarati, Kannada, Malayalam, Punjabi, and Urdu, which is RTL), plus Russian, Polish, Turkish, Vietnamese, Thai, Indonesian, Swedish, and Ukrainian. Each is a complete 171-key catalog with per-key
machine-draftprovenance, and Hebrew and Urdu reused the right-to-left mechanism built for Arabic with no code changes (catalog declares"direction": "rtl", the window mirrors). No GUI code changed; twenty catalog files landed and the selector discovered them. The localization gate now covers all 31 languages (198 checks): completeness, no orphan keys, valid provenance, format-placeholder parity, and declared text direction. One cosmetic note left for human review: the optional secret-expiry date hint shows a localized mask in some languages (AAAA-MM-DD,JJJJ-MM-TT) and the literalYYYY-MM-DDin others; both are internally consistent and preserve year-month-day order, and the validator accepts digits regardless of the mask shown. - Four more GUI languages, including the first right-to-left one: Korean, Japanese, Simplified Chinese, and Arabic. Eleven languages now ship. Arabic brought real RTL support to the architecture: a catalog declares its own text direction (
"_language": { ..., "direction": "rtl" }), the loader surfaces it, and switching to an RTL language flips the whole window'sFlowDirectionso WPF mirrors the entire layout; the run log and the source inspector stay left-to-right because cmdlet output and PowerShell source are English regardless of UI language. The credential dialog mirrors to match. The localization gate validates each catalog's declared direction and that the loader reports it. Still no GUI code paths per language, just the shared RTL switch and four more catalogs. - Five more GUI languages: Portuguese, German, Danish, Dutch, and Hindi. Each is a complete 171-key catalog (
gui.pt/de/da/nl/hi.json) with per-keymachine-draftprovenance, discovered by the header language selector at runtime, exactly as the architecture intended: no code changed, five files landed. The localization gate now generalizes to every shipped catalog instead of naming languages, and gained a placeholder-parity check: a translation that drops or invents a{0}format placeholder is a runtime format error in that language, so it is a red build. Brand and platform names (Guerrilla, Safehouse, PowerShell, Active Directory, Entra, Google Workspace) stay untranslated across all languages, and literal machine-parsed strings (date formats, dialog filters, the rotate command line) are preserved verbatim.
- The GUI is multilingual, starting with Spanish, on an architecture built to scale. Every user-visible string in Show-Guerrilla (window chrome, navigation, the Run page, options drawer, status and progress messages, dialogs, and the add-credential modal) now comes from locale catalogs in
Data/Locales/(gui.en.jsonis the English source;gui.es.jsoncarries all 171 keys with per-key translation provenance, the samemachine-draft/human-reviewedconvention the website uses). A language selector in the header switches the entire window live, exactly the way the theme toggle works (string resources swapped through WPF DynamicResource, no restart), and the choice persists in config asgui.languagenext togui.theme; first launch follows the OS display language when a matching catalog exists. Adding a future language is adding one catalog file: the selector discovers catalogs at runtime and nothing hardcodes a language list. Semantic values are decoupled from display text (localized ComboBox items carry canonicalTagvalues, so translating "Light" can never change what a cmdlet receives). A new gate,Tests/verify-gui-localization.ps1, fails the build if a GUI string reference is missing from the English catalog or if a shipped language is incomplete, and proves its own scanner can fail with an in-file poison self-test.
- Every generated report is rebuilt from scratch on the website's design system, so guerrilla.army, the desktop GUI, and the HTML reports now read as one product. All ten HTML outputs (the AD, Entra, and Google Workspace reports, the unified Campaign report, the trend report, the dashboard, the technical report, the executive summary, the remediation playbook, and the budget justification) share a single theme engine (
Get-GuerrillaReportTheme) whose light and dark palettes are a mirror of the site's contrast-verified design tokens, one shared component stylesheet (soft cards, quiet pill badges, stat tiles, light table rules, the system font stack), and one shared shell: a sticky header with the Guerrilla wordmark and a light/dark toggle, the white-label banner and firm header when branding is configured, and the standard footer. Reports default to following the viewer's OS theme (-ReportStyle Auto), and the in-report toggle switches instantly with the choice remembered per browser;LightandDarkforce an initial theme. The legacy style names remain accepted everywhere they were before, in the cmdlets, inoutput.reportStyleconfig, and in the sample generator:Professionalmaps to Light, andGuerrillaandSlatemap to Dark. Printing (andExport-ReportPdf) always renders the light palette. The interactive findings filter, run comparison, Security Maturity, Indicators of Exposure, Attack-Path Cartography, and attack-path sections carry over restyled; none of their logic changed, and the full report-section test suite passes unchanged. The GUI's report-style option now offers Auto, Light, and Dark.
- The GUI is rebuilt to look like guerrilla.army and to make running an audit a one-click act. Show-Guerrilla's window now follows the website's design tokens exactly (the same flat surfaces, pill buttons, 12px cards, near-invisible borders, and WCAG AA contrast-verified color pairs, mirrored in
Get-GuerrillaGuiTheme), with a light/dark theme toggle whose choice persists in your config; first launch follows the OS app theme. The window is borderless with its own header navigation and custom caption buttons instead of standard Windows chrome. The former Operations tab is now a Run page with exactly one button per platform (Active Directory, Entra/Azure/M365, Google Workspace, plus Campaign for everything); category selection, scan depth, report style, student OUs, and the output directory moved into an Options drawer with sensible defaults, and the run itself shows a live log with a progress sweep and a result callout. The Add-credential modal matches the new look and picks up the active theme. Everything underneath is unchanged: the GUI still wraps the same public cmdlets,-StartOnaccepts the same values, and the credential entry/validation helpers are untouched.
- The K12 Secure Configuration Baseline, a Guerrilla-authored candidate community baseline for school districts. School tenants hold adults and minors in one tenant with legally distinct duties toward each, and no consensus baseline assesses the boundary between them. This one proposes twelve controls that do: data protection and sharing, identity and third-party access, child safety, device posture, lifecycle, audit durability, and account hygiene. The baseline document (
docs/baselines/k12-secure-configuration-baseline.md) is versioned (0.1.0), openly published, open for comment, and explicitly NOT a consensus standard: it is expert opinion labeled as such. Checks that assess it carry aguerrillaBaselinefield that is deliberately separate from thecompliancefield used for external framework mappings, its shape is pinned by a schema gate so it can never grow into a lookalike framework mapping, and a completeness gate fails the build if a check claims a control the document does not define or the document claims coverage no check provides. Both directions, with an in-file poison self-test. - Student-OU scope as a first-class input. Student posture is an OU subtree, not a tenant-wide property, so the new
-StudentOUparameter (Invoke-GWSAudit and Invoke-Campaign with Workspace OU paths, Invoke-ADAudit with OU distinguished names, Invoke-EntraAudit reserved for the planned Entra twins) designates which subtree holds students. It is not a collection filter: collection stays tenant-wide so staff and student posture remain comparable. An OU-scoped check that is not given the scope reports Not Assessed with evidence saying the input is required; it never silently assesses the whole tenant as if that were the student population. The Show-Guerrilla Operations tab gains a Student OUs field (the window's first control with screen-reader automation properties). The OU scope is part of the run's comparison-series identity, so a student-scoped run is never diffed against a whole-tenant run and false drift is structurally impossible; pre-existing whole-tenant history keeps matching whole-tenant runs. - The first ten K12 checks (
GWS-K12-001throughGWS-K12-010), assessing ten of the twelve candidate controls: staff-default sharing inheritance, student external Drive sharing, third-party app authorization, vendor delegated access, delegated admin least privilege, student communication boundaries (Chat/Meet), guardian access integrity, managed Chromebook posture, departed-student disposition, and the age-banded account security floor. Every Policy API setting type, field name, and enum direction was verified against the Policy API catalog and the CISA ScubaGoggles assessment logic before verdict code was written. Effective values resolve per student OU by nearest ancestor with the API's documented precedence (highest sortOrder, ADMIN over SYSTEM, merged fields), and group-targeted policies surface as an evidence caveat rather than being silently attributed. Policy-dependent controls report WARN review lists, never invented hard failures; the child-safety checks grade configuration posture and say so in their evidence; 2SV enforcement is age-band context, never a blind FAIL of an elementary OU. 63 golden fixtures cover every declared verdict branch including the required no-scope and OU-absent branches of all eight OU-scoped checks. Suite: 636 checks, 1,829 fixtures, 0 failed. - Per-student-OU Chrome policy resolution. When
-StudentOUis provided, Chrome user-app and device policies are additionally resolved per student OU (same API, same already-delegated scope); a per-OU failure records its own error key so one OU going dark does not darken the rest.
-TargetOUruns no longer share a comparison series with whole-tenant runs. The collection scope was previously absent from the run-history series key, so an OU-narrowed run could become the baseline for a whole-tenant run and report scope differences as drift. OU scope (collection target plus student designation) now joins the series identity in the record, the index, retention grouping, and both baseline-lookup paths, pinned by tests in both directions including the no-index fallback and legacy pre-scope records.- A failed K12 category surfaces as lost visibility in campaign diffs like every other category (
Get-GuerrillaPlatformCheckFunctiontracks the new orchestrator entry, enforced by the existing source assertion).
- The report opens with what changed since your last run. Every completed assessment is recorded to a local, per-user run history (verdicts, evidence hashes, and scores; never raw evidence values, so the one artifact that accumulates forever cannot leak what the report contains; no accounts, no telemetry, no network). The next run's HTML report leads with the comparison: newly failing checks first, then LOST VISIBILITY (a check that was assessed before and is Not Assessed now is never rendered as no change; a check going dark is how revoked read access or a broken collector hides an attacker), then confirmed remediations, plus the score delta overall and per Zero Trust pillar and the Not Assessed count delta at equal prominence. Checks new after an upgrade are labeled NEW and checks removed by an upgrade are labeled RETIRED, never counted as transitions. A crashed or partial run is never recorded, so it can never become a comparison baseline, and the history store refuses to fork silently next to an existing one.
- Invoke-Campaign can answer "what changed" for the first time. The all-platforms run previously had no delta at all (its per-platform delta mechanisms were report-file based and silently disabled under the campaign's internal -NoReports). The unified engine replaces the three divergent legacy delta paths (the AD and GWS single-slot state files and Entra's newest-report comparison) with exactly one comparison mechanism, fully covered by golden fixtures including the silent-diff case: every transition present in the inputs must appear in the output, asserted by count equality against an independently computed union.
- Scheduled cadence documentation.
docs/scheduled-runs.mdshows Task Scheduler (Windows) and cron (macOS/Linux) examples, framed honestly: Guerrilla does not run in the background; you run it, and the report tells you what changed. - Gates must prove they can fail.
Tests/Invoke-GatePoisonSelfTests.ps1injects a failure through each release gate's literal invocation shape and requires a non-zero exit, for gate A (fixture suite, via a new-PoisonSelfTestswitch that refuses to emit artifacts), gate B (collector contracts), and gate D (full unit suite); gate C already self-tests in-file. Wired intoPublish-Release.ps1and CI. Gate B now aborts instead of warn-and-skip when its test file is missing.
- The platform audits are named what they are.
Invoke-ADAudit(Active Directory),Invoke-EntraAudit(Entra ID / Azure / Intune / M365), andInvoke-GWSAudit(Google Workspace) replace the codenamesInvoke-Reconnaissance,Invoke-Infiltration, andInvoke-Fortification; the old names remain as deprecated wrappers that forward all arguments, warn once per session, and will be removed in the next major version. The codenames named attacker phases rather than platforms, the mapping had to be memorized, and the confusion was real: two internal surfaces (the localization strings and the unified dashboard's coverage buckets) carried the mapping inverted. The abstract concept "theater" is likewise renamed to "platform" throughout:-Theaters/-Theaterparameters are now-Platforms/-Platform(old parameter names still bind via aliases), the GitHub Actionplatforminput takesAD | Entra | GWS | Campaign(legacy codename values accepted and mapped with a deprecation notice), and report headings print Active Directory, Entra ID / M365, and Google Workspace. - Gate artifact schema v2.
test-summary.jsonnow carriesplatform(AD | Entra | GWS) per check instead oftheater; EIDSCA fixtures, previously unlabelled, are labelledEntra. The Zero Trust schema gate rejects anytheater-like field in a check definition (with a built-in poison self-test), so drift back to the retired concept is a red build. - Upgrade continuity. Delta state and previous-scan exports written under the old names (
reconnaissance-state.json,fortification-state.json,infiltration-<tenant>-*.json) are still read via legacy fallbacks, so the first post-upgrade run keeps its baseline instead of re-baselining. New state is written underad-audit-state.json,gws-audit-state.json, andentra-<tenant>-*.json. - The score labels say risk, not war. The six score tiers are now a plain risk ladder: Low Risk (90-100), Moderate Risk (75-89), Elevated Risk (60-74), High Risk (40-59), Severe Risk (20-39), Critical Risk (0-19), each describing distance from the assessed baselines. This retires the tactical labels (FORTRESS through OVERRUN in the composite score, and a second, inconsistent set in the per-platform audits) and the dual-lexicon report machinery that swapped them per theme: there is now exactly one label set, computed in one place, identical in console output, HTML reports, JSON exports, and the composite score. Score thresholds and colors are unchanged.
- The behavioral monitoring subsystem is removed. Guerrilla is a point-in-time assessment tool with a best-in-class report; it is not a monitoring platform and does not run in the background. Removed: the four monitors (
Invoke-Surveillance,Invoke-Watchtower,Invoke-Wiretap,Invoke-Lookout),Send-Signaland its eleven provider variants, the Patrol scheduler (Register-Patrol,Unregister-Patrol,Get-Patrol), the Google Workspace behavioral compromise scan (Invoke-Recon,Invoke-ReconDemo,Get-DeadDrop),Update-ThreatInteland the threat-intel data files, the GUI Patrol and Signals tabs, the detection-tuning and alerting parameters ofSet-Safehouse, and every internal, test, alias, and format view that served them. The exported surface drops from 67 commands to 34. The ideas worth keeping (audit-log evidence for controls with no configuration API, config-declared vs. actually-happened reconciliation, visibility loss as a finding) are recorded indocs/proposals/effective-state-and-audit-log-inference.md. - The Guerrilla Score is honest about what it no longer measures. The retired Threats component scored monitoring detections; with monitoring gone it would have silently awarded 30 percent of the composite for threats never assessed. The composite is now Posture (70 percent), Coverage (15 percent, across the three assessment platforms), and Trend (15 percent). Coverage also no longer misclassifies Google Workspace
ADMIN-*checks as Active Directory.
- Eight Google Workspace SCuBA configuration controls, each verified against the CISA ScubaGoggles Rego assessment logic before it was written (not from baseline prose, which is misleading for the inverted-semantics controls).
EMAIL-030(GWS.GMAIL.11.1) warns on automatic email forwarding;EMAIL-031(GMAIL.15.1) on enhanced pre-delivery message scanning disabled.DRIVE-017(DRIVEDOCS.1.8) on default file access not set to private-to-owner.ADMIN-019(COMMONCONTROLS.15.2) on data processing outside the storage region;AUTH-018(COMMONCONTROLS.8.2) on account self-recovery for users and non-super-admins.ADMIN-020(COMMONCONTROLS.10.4) fails when access to unconfigured third-party apps is not blocked (compliant valueBLOCK_ALL_SCOPES).ADMIN-021(COMMONCONTROLS.16.1) warns when additional Google services without an individual control are not restricted. Google models this with inverted semantics:serviceState = ENABLEDmeans the restriction is on, which is the compliant "services off" outcome. Confirmed against the Rego and commented in-code so it is not mistakenly reversed later.ADMIN-022(COMMONCONTROLS.16.2) warns when Early Access applications are enabled.- All read Cloud Identity Policy settings, weakest-OU-wins, absent policy = Not Assessed. GWS SCuBA controls assessed rose from 87 to 95.
- Branch-coverage gate on verdict logic. A check may declare its verdict paths (
verdictPaths);Invoke-FixtureTests.ps1fails unless a fixture exercises each declared path. A multi-field check must now prove every branch, not merely that one fixture passed.Test-GwsPolicyEnumgained a-NonCompliantValues(bad-list) mode so a control that fails on one specific value, and passes everything else, is mirrored exactly rather than approximated by an allow-list.
- GRLA provenance schema on every check definition. Four fields on all 618 checks:
provenance(baseline|original|build-ahead),source_url,source_read_date,official_id. Seven checks are classifiedoriginal, the attack paths no configuration baseline models yet (domain-wide delegation org-takeover, super-admin sprawl and super-admin-equivalent roles, over-scoped OAuth grants, sensitive-scope connected apps, and partner/GDAP delegated access). No check ID was renamed. A newTests/Unit/ProvenanceSchema.Tests.ps1gates it: an invalid provenance, anoriginalcarrying anofficial_id, or abuild-aheadmissing its source is a red build. - Derived test-summary artifact.
Invoke-FixtureTests.ps1 -EmitSummarywritestest-summary.json(check count, executed fixtures, pass/fail, module version, git SHA, and per-check verdict scenarios) and fails if the fixture-file count does not equal the number executed. The CI gate emits and uploads it. This is the single derived source the public documentation renders every count from, so a published number can only change when a green run proves it.
- Documentation. Full README rewrite leading with what Guerrilla is (agentless, read-only, PowerShell 7), the three theaters, 618 checks, and the golden-fixture discipline. CONTRIBUTING rewritten as a contribution ladder (report a wrong verdict, propose a check, contribute fixture data, write the check and its fixtures), with rung-matched GitHub issue templates.
- SCuBA crosswalk hygiene. CISA consolidated the Exchange Online baseline numbering and moved several topics into the Defender baseline. Four Exchange Online SCuBA tags are remapped to the current CISA Defender controls (required alerts, alerts destination, unified audit logging, audit retention); 24 stale tags that reference controls with no clear current equivalent are removed rather than guessed. No check verdict logic changed.
- Renamed the module
PSGuerrilla→Guerrilla. First release under the new name (repository and PSGallery module bothGuerrilla); the oldPSGuerrillapackage remains on the Gallery frozen at its last version. Existing installs migrate automatically and transparently: the per-user data directory (reports, config, patrol state) is carried forward one-time from…/PSGuerrillato…/Guerrillaon first use, and safehouse credential resolution falls back to a legacyPSGuerrillaSecretManagement vault when the new defaultGuerrillavault has no value. No manual re-registration is required.
- Ten Google Workspace SCuBA controls closing the last config-automated gaps — verified zero concessions. A rigorous setting-level diff against ScubaGoggles' Rego (every configuration setting it reads vs every one Guerrilla reads) surfaced real gaps the earlier family-level pass missed. Now closed: EMAIL-025 (GMAIL.1.1 mail delegation), EMAIL-026 (GMAIL.9.1 POP/IMAP access — a modern-auth/MFA bypass), EMAIL-027 (GMAIL.8.1 user email/contacts import), EMAIL-028 (GMAIL.10.1 Workspace Sync for Outlook), EMAIL-029 (GMAIL.18.1 spam-override sender lists), DRIVE-014 (DRIVEDOCS.4.1 Drive SDK API access — a data-exfil channel), DRIVE-015 (DRIVEDOCS.1.9 external-file sharing warning), DRIVE-016 (DRIVEDOCS.3.1 file security update), ADMIN-017 (COMMONCONTROLS.10.3 internal apps auto-trust), ADMIN-018 (COMMONCONTROLS.15.1 data-at-rest region). All read Cloud Identity Policy settings, weakest-OU-wins, absent policy = Not Assessed, tagged with GWS.* IDs. 30 golden fixtures. After this, Guerrilla reads every configuration setting ScubaGoggles reads (or assesses the same control via a more thorough mechanism — e.g., per-user forwarding enumeration); the only unautomated controls are ones ScubaGoggles does not automate from configuration either.
- Four Google Workspace SCuBA controls that close the last ScubaGoggles config-coverage concessions. These are the remaining controls ScubaGoggles evaluates from a configuration setting that Guerrilla did not: COLLAB-017 (GWS.CALENDAR.3.1) warns when Calendar interoperability is enabled (calendar data bridged to an external system); COLLAB-018 (GWS.CALENDAR.4.1) warns on paid appointment schedules; COLLAB-019 (GWS.MEET.5.1) warns when Meet automatic recording is on by default (meeting capture without a deliberate decision); GROUP-006 (GWS.GROUPS.4.1) warns when groups can be hidden from the directory (transparency). All read Cloud Identity Policy settings, weakest-OU-wins, absent policy = Not Assessed, tagged with GWS.* IDs. 12 fixtures. After this, every GWS SCuBA control ScubaGoggles derives from configuration is covered; the only controls left unautomated (Chat content-reporting, Meet Gemini settings, Calendar interop-management manual step) are ones ScubaGoggles does not config-automate either — manual or audit-log-derived for both tools.
Consolidated PSGallery release carrying everything accumulated since 2.40.1 — the SCuBA EXO + CIS reconciliation closes, Entra ID Governance entitlement-management hygiene, Copilot Studio AI-agent governance, and the full Google Workspace SCuBA baseline closes (Gmail, Groups, Chat, Meet) with provable GWS.* crosswalk tagging.
- Release process: publishing now tags and creates a GitHub release.
Publish-Release.ps1, on a real publish, creates an annotatedv<version>tag, pushes it, and opens a matching GitHub release (idempotent — skips if the tag exists). This reconciles the repository with the PSGallery, which had diverged (git tags had frozen at v2.9.x and releases at v2.39.0 while the Gallery advanced), and was surfaced by live validation.
- Five Google Workspace tail SCuBA controls (Chat / Meet / Groups). COLLAB-013 (GWS.CHAT.2.1) FAILs when Chat external file sharing is enabled (a data-exfiltration path); COLLAB-014 (GWS.CHAT.3.1) WARNs when Chat space history is not always-on; COLLAB-015 (GWS.MEET.2.1) FAILs when meeting join is not restricted to the organization (external parties joining student-present sessions); COLLAB-016 (GWS.MEET.5.2) WARNs when Meet automatic transcription is on by default (data minimization); GROUP-005 (GWS.GROUPS.3.1) WARNs when the default group-conversation visibility is broader than members. All read Cloud Identity Policy settings, weakest-OU-wins, absent policy = Not Assessed. 15 golden fixtures. This closes the mission-relevant remainder of the Google Workspace SCuBA baseline; the residual controls with no configuration API (Gemini-in-Meet, hide-from-directory, content-reporting, calendar interop/payments) are honestly out of automated scope.
- Google Workspace Groups sharing controls (4 checks). Externally-accessible Google Groups are a direct data-exposure path — in a school, that is student and staff data sitting in group content and archives. GROUP-001 (GWS.GROUPS.1.1) FAILs when group sharing is not limited to domain users; GROUP-002 (1.2) FAILs when owners can add external members; GROUP-003 (1.3) FAILs when groups can receive mail from the public (an inbound phishing vector); GROUP-004 (2.1) WARNs when group creation is not restricted to administrators. All read the
groups_for_business.groups_sharingpolicy from the Cloud Identity Policy API; an absent policy is Not Assessed. This was the one Google Workspace SCuBA baseline area with no coverage at all. 13 golden fixtures.
- Two Google Workspace Gmail SCuBA controls. EMAIL-023 (GWS.GMAIL.12.1) flags per-user outbound gateways — users routing outbound mail through their own external SMTP servers, a data-exfiltration and spoofing path that bypasses org mail controls — read from the Cloud Identity Policy API. EMAIL-024 (GWS.GMAIL.16.1) flags a disabled Gmail Security Sandbox (virtual attachment detonation for zero-day malware); its policy field is best-effort pending live confirmation, so it reports Not Assessed rather than a fabricated verdict when the setting is not returned. 8 golden fixtures.
- GWS SCuBA baseline crosswalk. Tagged the Gmail attachment-protection, link-safety, and spoofing/authentication checks (EMAIL-015/016/017) with the specific GWS.GMAIL.5.x / 6.x / 7.x SCuBA control IDs they satisfy — 14 controls previously covered but untagged. Coverage is now provable by control ID (the check reads the same Cloud Identity setting the control specifies), not merely by concept.
- Copilot Studio AI-agent governance (new category, 4 checks). Copilot Studio agents front organizational data and tools yet are a fast-growing, rarely-governed surface. A new
AIAgentcategory collects agents from Dataverse (thebotstable per Power Platform environment) and grades them: AIAGENT-001 FAILs agents whose access control isAny/Any multitenant(interactable by anyone, even cross-tenant); AIAGENT-002 FAILs agents that accept anonymous interaction and WARNs those whose authentication is only triggered "as needed"; AIAGENT-003 WARNs published agents left unmodified past a dormancy threshold; AIAGENT-004 WARNs authenticated agents not scoped to security groups. Verdict logic is fixture-proven (15 golden fixtures); the Dataverse collector (Global Discovery Service → per-environmentbotsquery) is contract-tested for its endpoints. Collection requires live validation on a Power Platform tenant (the per-environment Dataverse auth flow and the authentication-mode field mapping have not yet been exercised live); until then, and whenever agent data is not collected, the checks report Not Assessed rather than a fabricated verdict. First tranche — agent topic/tool inspection (hard-coded credentials, connector consent) is deferred to a later tranche.
- Entra ID Governance — entitlement-management hygiene (new category, 5 checks). Access packages are a standing-grant mechanism that is rarely reviewed after creation; Guerrilla previously did not inspect them at all. A new
Governancecategory collects entitlement-management assignment policies and catalogs and grades them: EIDGOV-001 flags assignment policies that grant access without approval; EIDGOV-002 flags policies without recurring access reviews; EIDGOV-003 flags perpetual (never-expiring) assignments; EIDGOV-004 FAILs when a policy allows external/all-users eligibility without approval (WARNs when approval-gated); EIDGOV-005 flags externally-visible catalogs for review. A failed collection is Not Assessed; an empty-but-collected result means entitlement management is simply not in use (PASS, nothing to govern). Backed by a collector query-contract test (asserts the threeentitlementManagementendpoints) and 18 golden fixtures. Field names for entitlement-management sub-settings are best-effort pending live validation on a governance-licensed tenant; absent fields degrade to the safe branch, never a fabricated verdict.
- Two SCuBA Exchange Online baseline controls. M365EXO-049 (MS.EXO.9.5) confirms the anti-malware Common Attachment Filter blocks executable attachment types (.exe/.cmd/.vbe), the most common malware-delivery payloads, on every anti-malware policy. M365EXO-050 (MS.EXO.8.4) confirms Data Loss Prevention rules restrict sharing of U.S. Social Security numbers, ITINs, and credit-card numbers via email. Because
Get-DlpComplianceRulecannot distinguish an unconfigured DLP solution from one that could not be read, absent or empty rule data is reported Not Assessed rather than a false FAIL or a pass — a FAIL is only returned when rules are present but demonstrably miss a required type. M365EXO-019 (DMARC aggregate-report contact) is additionally mapped to MS.EXO.4.4, a control it already satisfied. Backed by 10 golden fixtures.
- Gemini audit-log derivation matched the wrong setting literals. Live-tenant validation showed Google names generative-AI admin-audit settings
gen_ai_*(which contains neither "gemini" nor "generative") and emits them underCHANGE_CHROME_OS_USER_SETTING, not only*_APPLICATION_SETTING— so the 2.40.0 scoping inConvertTo-GeminiDerivedSettingsmatched nothing, and GWS-GEMINI-002/003/004/005 would SKIP even when a setting had been changed. Scoping is broadened togen_ai/gemini/generativeover any*_SETTINGcreate/change event; a two-level match (gen-AI scope then per-setting sub-pattern) keeps it safe — the observedgen_aiwallpaper/image settings map to no target, so no spurious verdicts. Still inference, still labeled as such on every verdict; the exact Workspace-GeminiSETTING_NAMEliterals (and value param) remain pending one live change event to confirm, so the derivation continues to SKIP safely on no match.
- Partner / GDAP delegated-access review (2 new Entra checks). Granular Delegated Admin Privileges (GDAP) let a CSP or managed-services partner hold standing admin roles in your tenant — invisible in most consoles, rarely reviewed, and the Kaseya-class propagation path (compromise one partner, inherit delegated admin across every downstream tenant). EIDTNT-015 inventories active
delegatedAdminRelationshipsand FAILS when any grants a Tier-0 / high-impact directory role (Global Admin, Privileged Role/Authentication Admin, Security Admin, User/Password/Application/Cloud Application Admin), WARNs on non-privileged partner access, and PASSes only when there is none. EIDTNT-016 flags long-lived grants — relationships that auto-extend beyond a year renew themselves without review. Both carry a Zero Trust stance (Identity/Governance) and, critically, treat a failed collection as Not Assessed rather than a clean pass. Backed by a collector query-contract test and 10 golden fixtures (including the terminated-relationship and collection-failure edge cases). - Gemini deep-settings coverage via audit-log inference (GWS-GEMINI-002/003/004/005). These four settings (Alpha features, conversation history, retention, sharing) are exposed by no Google config or policy API. Rather than a blanket SKIP, Guerrilla now infers their state from Google Admin audit-log setting-change events — the same source CISA ScubaGoggles derives them from — and labels every such verdict as inferred (with the source event's timestamp), never as a direct config read. When no change-event exists in the audit-log retention window the state is genuinely unknowable (to any tool), so the check honestly SKIPs. A pure derivation function (
ConvertTo-GeminiDerivedSettings) is unit-tested for most-recent-wins, value normalization, and the safe-absence fallback; 8 new fixtures cover the inferred verdicts.
- DEVICE-009 false-WARN on a tenant with zero Chrome OS devices. An empty
ChromeDevicesarray was hitting the empty-array truthiness trap (-not @()is$true) and short-circuiting to a spurious warning instead of the author's own "no Chrome OS devices registered → PASS" branch. Collection failure is still correctly Not Assessed via the source guard.
- Zero Trust posture, first-class. Every check now declares a
zeroTrustPillar(CISA ZTMM v2.0) andzeroTrustWeight(0–3) in its definition, and those flow onto every finding — so(Invoke-Infiltration …).Findings | Where-Object ZeroTrustPillar -eq 'Identity'and pipeline-computed pillar scores work end to end, not as a report-side decoration. New public functionGet-ZeroTrustScorerolls findings into a weighted posture score per pillar (credit: PASS 1.0 / WARN 0.5 / FAIL 0; Not-Assessed excluded from the denominator so uncollected controls never skew it) and reports a CoverageConfidence — Solid / Moderate / Directional — so a score computed from a thin pillar (e.g. Data) says so instead of reading as authoritative. The Infiltration HTML report renders the per-pillar posture line on its cover. - A Zero Trust schema test (wired into CI and the release gate) fails the build if any check omits its pillar or weight, making "declare your ZT stance" mechanically un-skippable at authoring time.
- Description narrowed "Azure" → "Azure identity-plane" to match actual coverage (identity/config posture, not full resource-plane CSPM).
- Three collector under-fetch defects that produced wrong verdicts on live tenants. The check logic was correct (offline fixtures passed) but the collector fed it incomplete data, a class the golden-fixture harness structurally cannot catch:
- EIDTNT-005 (cross-tenant access) — false PASS.
Get-EntraTenantDatafetched the/policies/crossTenantAccessPolicycontainer, which carries nob2bCollaboration*settings, so a tenant whose default policy allows all inbound + outbound B2B collaboration scored clean. It now also collects/crossTenantAccessPolicy/defaultand exposes it under.default, so the permissive default is correctly flagged. - INTUNE-005 (device configuration profiles) — false FAIL.
Get-IntuneDatafetcheddeviceConfigurationswithout$expand=assignments, so every profile looked unassigned. It now requests assignments. - EIDPIM-010 (PIM configuration) — false FAIL.
Get-EntraPIMDatareadroleEligibilityScheduleInstances(which can be empty while eligibility is configured); it now readsroleEligibilitySchedules(definitions), so standing eligible assignments are detected instead of reporting "PIM not configured".
- EIDTNT-005 (cross-tenant access) — false PASS.
- Empty-collection dead-branch across ~10 checks.
if (-not $x.Collection)is$truefor a present-but-empty array, so a connected tenant with zero of something short-circuited to SKIP/WARN and the meaningful verdict was unreachable. Fixed in M365EXO-002/005/006/007/029/030/038, DEVICE-001, LOG-002, and ADGPO-001: a connected tenant with zero anti-malware/anti-spam policies now FAILs, zero Google alert rules FAILs, zero mobile devices PASSes, etc. Not-Assessed-on-collection-error is preserved (theGet-NotAssessedFindingerror-map guard still fronts each check). - Two AD checks had no reachable PASS / honest path. ADGPO-017 (Restricted Groups) now PASSes in the clean state (was always WARN); ADCS-012 (ESC9 binding enforcement) reports Not Assessed when the registry value wasn't collected (was a permanent WARN), keeping PASS reachable when the value is present.
- Alerting / patrol reliability.
Send-Signalno longer throws "array index evaluated to null" when piped monitor results (Watchtower / Surveillance / Wiretap) that exposeSeverityinstead ofThreatLevel—Severityis normalized toThreatLeveland the level lookup isContainsKey-guarded, so alerts are actually sent.Format-SignalContentno longer aborts delivery on all channels when a result'sTimestampis null.Register-Patrolno longer prints "Created scheduled task" after a registration that actually failed (the call is wrapped in try/catch and surfaces an elevation hint).
- Collector query-contract tests (
Tests/Unit/Private/Entra/CollectorQueryContract.Tests.ps1) — mockInvoke-GraphApiand assert the exact Graph endpoints/parameters each collector requests (e.g./crossTenantAccessPolicy/default,$expand=assignments,roleEligibilitySchedules). This catches collector under-fetch, which the golden fixtures cannot, since they feed the check hand-built data and never invoke the collector. - Regression fixtures for the corrected verdicts: EIDTNT-005 re-shaped to the real
.defaultGraph layout; newempty-scenario fixtures (zero-policy ⇒ correct verdict) andthrottledfixtures (uncollectable ⇒ Not Assessed) for the affected Exchange checks; updated ADGPO-001/017 and LOG-002 fixtures.
Read-only; no check-count or public-surface change. 580 checks; 49 public functions.
- 7 Entra PIM privileged-account checks now return real verdicts. EIDPIM-004/005/006/007/008/009/013 referenced an undefined
$privilegedUsersand threw at runtime (surfacing as ERROR) instead of evaluating. They now read the collected privileged-user set from$AuditData.PIM.PrivilegedUsersbehind aGet-NotAssessedFindingguard, and correctly flag guest accounts in privileged roles (004), on-premises-synced admins (005), privileged users without MFA (006) or with weak-only MFA (007), disabled accounts that still hold privileged roles (008), never-signed-in privileged accounts (009), and adherence to a separate-admin naming convention (013). Each returns Not Assessed when the privileged-user data was not collected.
- Golden-fixture detection-test coverage for all 44 EIDSCA controls — driven through the real
Invoke-EntraEidscaChecks/Resolve-EidscaControlpath (pass / fail / not-collected per control) — and for the 7 repaired PIM checks. The suite is now 1,583 fixtures across every fixturable check. (Repository/test-only; the EIDSCA harness adds anInvoke-EntraEidscaChecksdispatch branch to the fixture runner.)
- Golden-fixture detection-test suite. The first tests that validate the checks' verdict logic (prior tests covered alerting, the vault, and scheduling only). 468 of 580 checks — every Critical, High, and Medium severity control across Active Directory, Entra ID / Azure / Intune / M365, and Google Workspace — are now pinned by synthetic JSON fixtures (
Tests/Fixtures/) that pump known-good, known-bad, and uncollectable/throttled data through the realTest-Recon* / Test-Infiltration* / Test-Fortification*functions and assert the returned status: clean ⇒ PASS, known-bad ⇒ FAIL/WARN, no-data ⇒ Not Assessed (SKIP). 1,288 fixtures run under Pester in a few seconds viaTests/Unit/GoldenFixtureChecks.Tests.ps1, with a CI-gating runner (Tests/Invoke-FixtureTests.ps1) and optional run-history tracking. The SKIP cases are a regression guard for the "absence of evidence scored as compliance" failure mode. Building the suite surfaced several latent verdict defects (empty-array guards swallowing PASS/FAIL into SKIP, a few checks with no reachable PASS path, and a broken Entra PIM check family), which are tracked for fixing. Repository/test-only addition — the installable module surface and all check behavior are unchanged.
- Uncollectable controls now report "Not Assessed" instead of PASS. When a data source fails to collect (Graph throttling/errors, AD enumeration failures), checks across all theaters surface the gap as a SKIP rather than scoring it compliant.
Invoke-GraphApifails loud by default so collectors record the failure in their error map (continuous-monitoring collectors opt out via-ReturnNullOnError); a sharedGet-NotAssessedFindingguard fronts the checks, and ~285 check sites consult that error map before passing on empty data. Read-only; no check-count or public-surface change.
- Signals tab in the GUI (
Show-Guerrilla) — manage alert providers without leaving the window. Add / remove / test Microsoft Teams, Slack, generic Webhook, PagerDuty, Pushover, SendGrid, Mailgun, Twilio, Syslog, and Windows Event Log signals; provider secrets are stored in the vault using the same keys and formats the CLISend-Signalpath reads (string for webhook-style providers, JSON for email/SMS), so the GUI and CLI stay in sync. The tab also exposes alerting on/off, the minimum threat level, and duplicate-alert suppression (window hours) viaSet-Safehouse. The per-provider Test button sends a synthetic alert through the realSend-Signal<Type>path to confirm delivery. New-StartOn Signalsoption and aShow-AddSignalDialoghelper.
- Professional is now the default report style for all HTML reports (
Invoke-Campaign/Invoke-Reconnaissance/Invoke-Infiltration/Invoke-Fortificationand theExport-*ReportHtmlbuilders). Pass-ReportStyle Guerrilla(orSlate) to opt out. - Findings list their affected entities as a bulleted list (
<ul><li>) instead of a comma-separated paragraph. A new sharedGet-GuerrillaReportAffectedHtmlrenderer is wired into all four theater reports (it surfaces theAffectedItems/AffectedLabelconvention and auto-detected scalar arrays from a finding'sDetails, capped at 25 with a "+N more" bullet).
- Single-instance guard is now advisory instead of absolute. A pre-fix launch whose window got lost behind the hidden console leaves a live process holding the OS mutex (not abandoned, so 2.32.1's reclaim doesn't apply) — which permanently blocked new launches with "already open in another window." Now, when the lock is held, you get a Yes/No prompt to open a new window anyway (proceeding without the lock; only the genuine two-live-windows case risks state clobbering, and you're told).
- Window comes to the front on launch (
Activate+ briefTopmostonContentRendered) so it can't open hidden behind other windows — the condition that strands it when the console is also hidden.
- GUI single-instance guard falsely reported "Guerrilla is already open in another window." The old guard used
Mutex(initiallyOwned, …, [ref]$createdNew)and blocked whenever the named mutex still existed — so a launch that closed abnormally or was force-killed (more likely now that the console is hidden) left the handle open and permanently blocked new launches. The guard now self-heals: it disposes a stale handle from the current session, reclaims an abandoned lock from a dead process (WaitOne(0)+AbandonedMutexException), and always releases the lock on close via afinally.
Operations Console redesign + console-hide.
Show-GuerrillaGUI restyled to a light, modern, clean enterprise theme — white cards, a blue accent (#2563EB), rounded corners, Segoe UI typography, subtle borders, and corrected ComboBox/DataGrid contrast (the dropdown/grid text now stays readable on light surfaces). Colors are centralized inGet-GuerrillaGuiTheme.ps1; layout, control names, and event logic are unchanged.
- Console-hide: the host PowerShell console is hidden while the GUI is open and restored when it closes. New
-KeepConsoleswitch onShow-Guerrillakeeps the terminal visible (useful for debugging). Windows-only; the CLI is unchanged.
Three AD collectors that turn previously Not-Assessed checks into real verdicts on a domain controller. Each degrades to Not Assessed when its data/rights/module are unavailable — never a false pass.
- NT-hash password quality (
Get-ADPasswordHashQuality) — replicates hashes via DSInternals (DCSync) and runsTest-PasswordQuality. Lights up blank-password (ADPWD-010) and duplicate-password (ADPWD-011) detection, and privileged weak passwords (ADPRIV-016). HIBP/dictionary/common (ADPWD-012/013/014) stay Not Assessed unless a dataset is supplied. Security: only account names + counts are kept; NT hashes/cleartext are analysed in memory and never written to the result, disk, or pipeline. - Replication health (
Get-ADReplicationHealth) —Get-ADReplicationPartnerMetadata/Get-ADReplicationFailure(orrepadmin), feedingADDOM-007; a single-DC forest is reported healthy. - DC user-rights assignment (
Get-ADUserRightsAssignment) — parses the Domain Controllers security template forSeInteractiveLogonRight/SeRemoteInteractiveLogonRightand flags non-Tier-0 principals, feedingADPRIV-026(local logon) andADPRIV-027(RDP).
Honesty fix — six AD checks could report PASS without actually performing the assessment.
- DSInternals NT-hash password checks (
ADPWD-010,-011,-012,-013,-014) — these treated DSInternals being installed as analysis performed and returned PASS ("no blank/duplicate/HIBP/dictionary/common passwords") against a result field that no collector populates. They now return Not Assessed when the NT-hash analysis was not actually run (no hash dataset collected — requires replication / ntds.dit access), and still FAIL/PASS correctly once a real dataset is present. - AD CS ESC6 (
ADCS-009) — read the LDAPpKIEnrollmentServiceflagsattribute, which cannot carry theEDITF_ATTRIBUTESUBJECTALTNAME2policy-module registry bit, so it returned a false PASS ("not set"). It now reports Not Assessed with guidance (certutil -getreg policy\EditFlagson each CA host), since the flag isn't determinable via agentless LDAP.
Surfaced via live-domain Azure lab validation. Related coverage gaps that already SKIP honestly (ADDOM-007 replication health, ADPRIV-016 privileged-password strength, ADPRIV-026/027 DC user-rights) remain Not Assessed and are tracked as planned collector features.
Live-domain reliability fix (validated on a domain controller).
- AD well-known group resolution (
ADTRADE-008,ADTRADE-009, and any SID-based lookup) — the SID→binary conversion calledSecurityIdentifier.GetSidBytes(), a method that does not exist; it threw and was swallowed by the surrounding try/catch, so Cert Publishers / Key Admins / Enterprise Key Admins (and other RID-relative groups) reported Not Assessed even when present. Replaced with the correctGetBinaryForminGet-ADTradecraftSignals.ps1,Get-ADPrivilegedMembers.ps1, andResolve-ADSid.ps1. Confirmed against a live domain controller: the groups now resolve and the checks return real PASS/FAIL verdicts. (2.30.1 mis-attributed this to a byte[] objectSid issue —objectSidis already converted to a string upstream — so this is the actual root cause.)
Reliability fixes from live validation of the v2.30.0 checks. No check-count or public-surface change (580 checks).
- AD Tier-0 group resolution (
ADTRADE-008,ADTRADE-009) — the domain SID was read as a rawSystem.Byte[]and string-interpolated, producing a malformed SID so the RID-relative lookups for Cert Publishers / Key Admins / Enterprise Key Admins always failed and the checks reported Not Assessed. Now converted to the canonical SID string before use, so membership is evaluated correctly (an honesty-doctrine fix — a disguised SKIP could hide real members). - Entra Connect / hybrid identity (
EIDFED-013and the federation family) — hybrid detection no longer depends solely on/directory/onPremisesSynchronization(which requiresOnPremDirectorySynchronization.Read.Alland returns 403 without it). It now falls back to the authorizedorganization.onPremisesSyncEnabledsignal and synced-user count, so a synchronized tenant is no longer misreported as cloud-only. The Azure AD Connect configuration review (EIDFED-005) no longer returns PASS when the sync configuration is unreadable — it reports Not Assessed and distinguishes a genuine cloud-only tenant from a hybrid tenant whose config is forbidden.
- Shadow-credential check (
ADTRADE-006) — distinguishes legitimate Windows Hello for Business / Entra hybrid device-registration keys on member computers (reported as WARN, review-only) from key credentials on user/admin principals or domain controllers (reported as FAIL, the real shadow-credential primitive). Eliminates the false positive on hybrid-joined estates while preserving detection of the actual attack.
+63 checks (580 total) — closing remaining framework-coverage gaps across all three theaters.
- Exchange Online depth (+36) —
M365EXO-013…M365EXO-048implementing the CISA SCuBA EXO baseline: anti-spam / anti-phishing / malware depth, Safe Links & Safe Attachments, mail-flow and external-forwarding controls, SPF/DKIM/DMARC, connection filtering, mailbox auditing, and audit-log retention. Extended the Exchange collector accordingly (Get-TransportConfig,Get-SharingPolicy,Get-HostedConnectionFilterPolicy,Get-HostedOutboundSpamFilterPolicy,Get-AcceptedDomain,Get-AtpPolicyForO365,Get-ExternalInOutlook, DNS mail-security resolution). - Active Directory indicators (+6) —
ADTRADE-005…ADTRADE-010: Seamless SSOAZUREADSSOACCKerberos key age (Silver-Ticket exposure), shadow credentials (msDS-KeyCredentialLink) on privileged objects, delegated-MSA migration escalation (BadSuccessor), Enterprise/Key Admins membership, Cert Publishers membership, and gMSA password-exposure posture. - Google Workspace SCuBA baselines (+15) — new Workspace Service Security category (
GwsService) covering Google Sites, Classroom, and Gemini, plus Assured Controls under Admin & User Management. Controls the Cloud Identity Policy API does not surface report as Not Assessed with Admin-console verification guidance. - Entra ID SCuBA completion (+5) —
MS.AADcontrols for Authenticator number-matching context, password-never-expires, group-owner app consent, risky-user notification (manual/Not-Assessed), and managed-device MFA registration. EIDFED-013— Entra Connect sync-client version currency — flags an outdated Entra Connect (Tier-0 hybrid component) against a minimum-safe baseline. Server-side read (registry /Get-ADSyncGlobalSettings) yields a definitive PASS/FAIL; cloud-only runs report Not Assessed with the server-side path. Includes a pure version comparator with unit tests.
- Counts: Active Directory 211, Entra ID / Azure / Intune / M365 244, Google Workspace 125 = 580 checks; 49 public functions. Read-only.
- Honesty preserved: every control whose data cannot be collected returns Not Assessed (SKIP/WARN) — never a pass.
Documentation cleanup — no functional change.
- Re-articulated the EIDSCA checks to describe each control by its Microsoft Graph setting and recommended value (functional remediation, no external links). Removed third-party product references from shipped help text, check descriptions, and this changelog — describing every feature on its own merits. No code, check logic, scoring, or count changes (517 checks, 49 public functions).
Turnkey CI/CD — a GitHub Action and a severity gate for security-config-as-code.
Get-GuerrillaCIGate— decides whether a CI build should fail from findings + a-FailOnseverity threshold (Critical/High/Medium/Low/Any/None). FAIL gates (plus WARN with-WarningsAsFailures); SKIP / "Not Assessed" never gates. Returns{ShouldFail; GatingCount; GatingCheckIds}.action.yml— a turnkey GitHub Action (composite) at the repo root: installs the module, runs a chosen theater (Infiltration / Reconnaissance / Fortification / Campaign), publishes JUnit results viaExport-GuerrillaJUnit, and gates the build. The caller authenticates to the tenant/domain first; the Action does not handle auth.
- Builds on the JUnit primitive. Gating logic is unit-tested (
Tests/verify-ci-gate.ps1, 11/11); the Action itself wraps validated cmdlets — confirm end-to-end in a live runner. 49 public functions; 517 checks unchanged.
Release-notes maintenance — no functional change.
- Refreshed the PSGallery release notes. No code, check, or scoring changes — module behavior is identical to 2.28.0 (517 checks, 48 public functions).
Interactive findings filter in the AD report — completes the interactive report experience.
- Interactive findings filter in the Reconnaissance report — a live filter bar (status + severity buttons + text search) over both findings tables, matching what the Campaign report already had. New shared helper
Get-GuerrillaFindingsFilterHtml; finding rows are taggedgg-row/data-status/data-sev/data-textand filtered client-side (auto-opens collapsed categories so matches show, with a "no findings match" notice). Print-safe (the bar hides on print).
- This completes the interactive report work (filter + Indicators of Exposure). Report/presentation only — no engine, check, or scoring changes (517 checks, 48 public functions). Samples regenerated. Test:
Tests/verify-report-sections.ps1(39/39 — filter bar, tagged rows, and filter script present). - Remaining roadmap: deeper Exchange Online coverage (needs a live tenant) and additional Entra ID governance checks.
Indicators of Exposure — a ranked, severity-scored exposure view in every report.
- Indicators of Exposure (
Get-GuerrillaIndicatorsOfExposureHtml) — a ranked view of the estate's actual exposures, added to the Reconnaissance, Google Workspace, Campaign, and Technical reports. Each open (FAIL/WARN) finding becomes a named, severity-scored indicator with its blast radius (affected-object count); the list is ranked by severity → FAIL-before-WARN → impact, under a Critical/High/Medium/Low summary. It's the same data you already collect, presented the way a CISO expects to read it.
- Report/presentation only — no engine, check, or scoring changes (517 checks, 48 public functions). Samples regenerated (the Infiltration/Campaign samples now also include the 44 EIDSCA checks). Test:
Tests/verify-report-sections.ps1(36/36 — IOE ranking, severity ordering, empty-when-all-pass, plus presence in all four reports). - Remaining roadmap: interactive findings filter, deeper Exchange Online coverage (+ SCuBA MS.EXO baseline refresh), and additional Entra ID governance checks.
Security config as code — JUnit output for CI/CD pipelines.
Export-GuerrillaJUnit— converts any theater's findings (AD / Entra / M365 / Google Workspace) to JUnit XML, the format GitHub Actions, Azure DevOps, and GitLab render natively as pass/fail. One<testsuite>per category, one<testcase>per check: FAIL →<failure>(typed by severity), SKIP/ERROR →<skipped>("Not Assessed", never a silent pass), WARN passes with output (or-WarningsAsFailuresto gate on it too). Returns{Tests; Failures; Skipped; Passed}so a pipeline canif ($r.Failures) { exit 1 }.
- Guerrilla results now render natively in CI/CD pipelines, across all four theaters. Copy-paste GitHub Actions / Azure DevOps / GitLab templates are available; a dedicated GitHub Action is a follow-on.
- 48 public functions; check counts unchanged (517). Test:
Tests/verify-junit.ps1(14/14 — valid XML, per-category suites, FAIL/SKIP/WARN mapping,-WarningsAsFailures, XML escaping, gating counts). - Remaining roadmap: the interactive report (filter + Indicators of Exposure), deeper Exchange Online coverage, and additional Entra ID governance checks.
Conditional Access what-if simulation — the live Graph evaluate API, with pre-built attack scenarios.
Test-GuerrillaConditionalAccess— simulates a sign-in against the tenant's live CA policies viaPOST /beta/identity/conditionalAccess/evaluate(signInIdentity/signInContext/signInConditions) and normalizes the applied policies into a single verdict (Block / MfaRequired / CompliantDeviceRequired / PasswordChangeRequired / Grant / NotApplied / Unknown).Invoke-Infiltration -WhatIfUserId <guid>runs a pre-built attack-scenario matrix (legacy-auth, no-MFA cloud sign-in, high sign-in risk, high user risk, unmanaged device) against that user and grades each PASS/FAIL . Results land inConditionalAccess.WhatIfand driveEIDCA-015, which was a placeholder/inference and is now a real, authoritative simulation when a user is supplied.
EIDCA-015now reports live what-if results when available; without-WhatIfUserIdit falls back to the previous policy-config inference, clearly labeled as such (not a live simulation).
- The CA evaluate API is beta: any empty/unrecognised response normalizes to
Unknown→ the scenario grader returns SKIP = "Not Assessed", never a false PASS. (Same honesty rule; will need re-pinning if the API GAs with a changed shape.) - 47 public functions; check counts unchanged (517). Test:
Tests/verify-ca-whatif.ps1(19/19 — normalizer across response shapes, grader, scenario catalog, and EIDCA-015 live grading incl. no-data→SKIP). Live Graph POST validated separately on a tenant. - Remaining roadmap: deeper Exchange Online coverage (+ SCuBA MS.EXO baseline refresh).
Full EIDSCA coverage — the 44-control Entra ID Security Config Analyzer baseline, evaluated for real.
- Full EIDSCA baseline (44 controls) as a new
Eidscacategory covering the EIDSCA control set (AF/AG/AM/AS/AT/AV authentication-method controls, AP authorization-policy, CP/CR consent, PR password-protection, ST guest-group settings). Each control is evaluated against its documented Microsoft Graph setting (Graph object + exact property path + operator + expected value), defined inData/AuditChecks/EidscaChecks.json. - A data-driven evaluator (
Resolve-EidscaControl) runs the catalog against the raw Graph policy objects Guerrilla already collects (authenticationMethodsPolicy,authorizationPolicy,adminConsentRequestPolicy, directorysettings) — no new collection needed. Surfaced viaGet-ComplianceCrosswalk -Framework EIDSCAand the new category inInvoke-Infiltration.
- EIDSCA coverage went from 10 approximate tags → 44 controls evaluated. The interim
eidscatags on existing Entra checks (v2.22.0) were removed so the dedicated EIDSCA category owns the framework (no duplicate crosswalk rows). - Check count: 473 → 517 (Entra/M365 158 → 202). AD 205, GWS 110 unchanged.
- Honest by design: any control whose source policy/setting wasn't collected (scope/module not connected) returns SKIP = "Not Assessed", never PASS. Verified.
- Test:
Tests/verify-eidsca.ps1(18/18 — every source type + operator, FAIL on misconfig, SKIP on missing data, dispatcher + crosswalk). Offline-validated; live confirmation pending on a tenant. - Next: Conditional Access what-if and deeper Exchange Online coverage (the SCuBA MS.EXO baseline mapping was undercounted and needs a refresh).
Fixes from the v2.22.0 live-validation pass — the attack-path visuals now render on real data, and "not assessed" stops reading as "compliant."
- 🔴 Attack-path visuals rendered empty on real domains. The shared report code read
Details.Chains(onlyADPATH-002carries that) butADPATH-001exposes its rich objects underDetails.Paths, and the@($null).Count == 1gotcha defeated theAffectedItemsfallback — so both the Attack Paths to Tier-0 list and the Attack-Path Cartography SVG came up empty despite real escalation paths. A shared gather now reads both shapes, filters$nullexplicitly, excludes by-designExpectedservice-account paths, and derives hop count whenLengthis absent. Fixes all three reports (Reconnaissance / Campaign / Technical). Unit tests now exercise theADPATH-001Pathsshape andExpectedexclusion. - Compliance crosswalk silently dropped SKIP'd checks, making coverage read artificially low (e.g. ~24 surfaced vs ~72 tagged on a partial connection). SKIP findings now surface with
Status='SKIP'("Not Assessed") so the crosswalk distinguishes passed from not looked at; onlyERRORis dropped. (-FailOnlybehaviour unchanged.) - Maturity model rated all-SKIP categories as "Level 5 — Optimized" (absence of evidence scored as success — same class as the old GTRADE-001 false-PASS). An estate or category with no PASS/FAIL/WARN now reports Level 0 = "Not Assessed", never 5.
- BloodHound export now resolves well-known privileged groups (Domain/Enterprise/Schema Admins, builtin operator aliases, etc.) to their real SIDs — domain SID derived from member SIDs + well-known RID/alias tables — so they overlay SharpHound's nodes instead of landing as parallel
NAME:<group>nodes that break cross-tool pathfinding. - Full-domain ACL sweep now includes
organizationalUnitobjects, so OU delegation (full-control / WriteDacl / WriteOwner on an OU) is no longer invisible to the sweep.
- All report/honesty fixes — no check logic, scoring, or count changes (473 checks, 46 public functions). Credit: the live-validation pass on a ~19.5k-object domain + partial-connection tenant. Tests: report-sections 29/29, maturity 22/22, bloodhound 14/14, full-domain ACL 18/18, SCuBA 12/12.
CISA SCuBA baseline crosswalk — Guerrilla now produces a SCuBA secure-configuration mapping, not just prose references.
- CISA SCuBA crosswalk: 55 Entra/M365 checks now carry
scubacompliance tags mapping to the published CISA SCuBA baseline policy IDs (MS.AAD / MS.EXO / MS.SHAREPOINT / MS.TEAMS / MS.DEFENDER / MS.POWERPLATFORM).Get-ComplianceCrosswalk -Framework SCUBAemits per-policy mapping rows, and the Executive Summary auto-surfaces a "SCUBA: N gap(s)" chip. ~76% of the assessable SCuBA baseline mapped (72 of 95 policies; see the coverage matrix). Baseline IDs were taken from CISA's published SCuBA baselines, not fabricated. - EIDSCA tagging: 8 checks tagged with
eidscacontrol IDs;Get-ComplianceCrosswalk -Framework EIDSCA.
New-AuditFindingnow carriesScubaandEidscacompliance arrays through to finding objects — the one engine change required for the new tags to flow into the crosswalk.
- Honest by design: only tagged checks produce SCuBA rows. 23 baseline policies and all of Microsoft Power BI are not yet assessed (no Power BI checks exist) — a report says "not assessed" rather than implying full compliance. The GAP list feeds a future net-new-check release (S1.4).
- Tag-only change: no check logic, scoring, or count changes (473 checks, 46 public functions unchanged). Clean additive JSON diffs. Test:
Tests/verify-scuba-crosswalk.ps1(12/12 — tags survive finding construction, SCUBA/EIDSCA rows produced + filter, untagged checks emit none, no CIS/NIST regression).
Attack-Path Cartography — a native visual map of escalation routes to Tier-0.
- Attack-Path Cartography (
Get-GuerrillaCartographyHtml) — a native, in-report SVG node-link map of the escalation routes to Tier-0, laid out left-to-right by longest-path rank. Non-privileged starting points are red (⚑), already-privileged amber, Tier-0 objectives gold (★); shared targets (e.g. Domain Admins reached from multiple sources) converge into one node. It's built entirely from the attack-path chain data already in findings — no extra collection, no external tool — so it renders self-contained in the HTML. Added to the AD reconnaissance, Campaign, and Technical reports (renders only when AD attack paths exist; capped at 25 paths with a shown notice).
- Sits alongside the BloodHound export (a static picture in the report + the full interactive graph in BloodHound CE). The attack-path program is now complete — maturity model, transitive attack-path engine, full-domain ACL collection, BloodHound export, and cartography all shipped.
- Report/presentation only — no engine, check, or scoring changes. Check counts and 46 public functions unchanged. Samples regenerated to include the map. Test:
Tests/verify-report-sections.ps1(24/24).
All four HTML report types now carry maturity + attack paths; sample reports regenerated to match.
Export-TechnicalReport(the README-linked "all checks" report) now also includes the Security Maturity and Attack Paths to Tier-0 sections, so all four report types (Reconnaissance, Fortification, Campaign, Technical) are consistent.- Shared section accent colour made theme-portable (
--deep-orange, defined in every report theme) so the sections render correctly in the Technical report's standalone stylesheet. - Sample/showcase reports regenerated: every sample now shows maturity; the AD, Campaign, and Technical samples show full attack-path chains; the AD sample shows the BloodHound callout. Added a Campaign sample (
Samples/Campaign-AllFail.html) and a sample BloodHound export (Samples/Reconnaissance-BloodHound.json).Generate-SampleReports.ps1now also (re)generates the README root sample (Guerrilla-Sample-Report.html) so it can't fall behind the templates again.
- Report/presentation only — no engine, check, or scoring changes. Check counts and 46 public functions unchanged. Test:
Tests/verify-report-sections.ps1(18/18 — now covers all four report types). - Remaining: cartography (the visual domain/trust/attack-path map).
Reports now showcase what Guerrilla actually does — maturity, attack paths, and the BloodHound export are no longer buried.
- AD reconnaissance report (
Export-ReconnaissanceReportHtml) gains three sections:- Security Maturity (CMMI 1-5) — the maturity rating was previously only in the Executive Summary; a normal scan's HTML now shows it.
- Attack Paths to Tier-0 — renders the full transitive chains (
HelpDesk --GenericAll--> CORP-Helpdesk-Admins --MemberOf--> Domain Admins), non-privileged sources first, instead of the single buried finding-row preview. When none are found it names the coverage lever (-FullDomainAcl). - BloodHound Export callout — when
-BloodHoundPathwas used, the report shows the written file path and BloodHound CE import steps.
- GWS report (
Export-FortificationReportHtml) gains the Security Maturity section (GWS Adversary Tradecraft findings were already surfaced via the detailed-findings renderer). - Unified Campaign report (
Export-CampaignReportHtml) — the "one big report" — gains Security Maturity and Attack Paths to Tier-0 across all theaters, so the aggregate view is no longer missing the marquee features.
Invoke-Reconnaissancenow runs the BloodHound export before report generation so the HTML callout references a file that already exists.- New shared helpers (
Get-GuerrillaMaturitySectionHtml,Get-GuerrillaAttackPathSectionHtml) keep the three reports' sections identical and theme-agnostic.
- Report/presentation only — no engine, check, or scoring changes. Check counts and 46 public functions unchanged.
- Test:
Tests/verify-report-sections.ps1(15/15 — shared helpers in isolation, plus maturity/attack-path/BloodHound inclusion across the recon, GWS, and Campaign reports). - Remaining: cartography (the visual domain/trust/attack-path map).
Full-domain ACL collector — shallow one-hop findings become deep low-priv → Domain Admins chains.
- Full-domain ACL collector (
Get-ADFullDomainAcl, surfaced viaInvoke-Reconnaissance -FullDomainAcl). Where the existing collector reads ACLs on the six critical Tier-0 objects, this sweeps every group / user / computer / gMSA in the domain, parsing each DACL from the binarynTSecurityDescriptorin one paged LDAP query (no per-object DirectoryEntry bind — the per-object path doesn't scale to a domain). Dangerous, non-default control ACEs are merged intoACLs.DangerousACEs, so the transitive attack-path engine and the BloodHound export consume them unchanged.
- Chains now actually form. ACE records previously carried no
ObjectClassorObjectSID, so the transitive engine could never classify an ACE target as a group node (grp:) and chains dead-ended regardless of coverage. Every ACE the full-domain collector emits now carriesObjectClass+ObjectSID+ObjectName, so a principal withGenericAll/WriteDacl/WriteOwnerover a group anywhere in the Tier-0 membership closure produces a real transitive path (e.g.HelpDesk --GenericAll--> CORP-Helpdesk-Admins --MemberOf--> Domain Admins), and BloodHound keys the target node by SID.
- Opt-in (off by default — it is the heaviest read Guerrilla performs).
MaxObjectscap of 50000 with explicit truncation reporting (FullDomainTruncated+ a log line — never a silent cap). SID→name resolution is cached. Read-only throughout. - Detection vocabulary matches the critical-object pass (GenericAll/GenericWrite/WriteDacl/WriteOwner + dangerous extended rights + WriteProperty on dangerous GUIDs incl.
member,msDS-KeyCredentialLink, DCSync, ForceChangePassword), with self-ACE / SELF / CREATOR OWNER skips on top of the existing default-principal ignores; the engine still applies its own default-principal exclusion downstream. - 46 public functions. Test:
Tests/verify-fulldomain-acl.ps1(18/18 — dangerous-ACE predicate, theObjectClass/ObjectSIDchain fix end-to-end through the engine, a regression guard proving noObjectClass→ no chain, and SID-keyed BloodHound nodes). Check counts unchanged. - Remaining: cartography (the visual domain/trust/attack-path map). The next depth lever beyond this is full-domain group membership (control edges already land in the existing Tier-0 closure; all-group membership widens multi-control-hop chains through non-privileged groups).
BloodHound export — Guerrilla now feeds the best attack-path graph tool, free.
Export-BloodHoundData— exports the collected AD graph (privileged-group membership + dangerous ACLs) to a BloodHound CE OpenGraph file. Nodes are SID-keyed (overlay cleanly with native SharpHound data) and edges use BloodHound's native kinds (GenericAll,WriteDacl,WriteOwner,GenericWrite,AllExtendedRights,GetChanges,GetChangesAll,MemberOf) so BloodHound's built-in pathfinding works over them directly. Unlike the in-report engine, the export includes the full graph (no default-principal exclusion) — BloodHound does its own reachability analysis. Import via BloodHound CE > Administration > File Ingest.Invoke-Reconnaissance -BloodHoundPath <file>writes the export as part of a normal scan; the result object gainsBloodHoundPath.
- This makes Guerrilla a free BloodHound feeder — the agentless, quiet collector that also hands you a graph (SharpHound gets flagged by EDR; this doesn't touch endpoints). Exported edge coverage tracks ACL collection (the six critical Tier-0 objects + privileged membership today); the full-domain ACL collector (roadmap) widens it and the exporter consumes it unchanged.
- Read-only. 46 public functions. Test:
Tests/verify-bloodhound-export.ps1(12/12 — OpenGraph shape, SID-keyed nodes, native edge kinds incl. replication→GetChangesAll, MemberOf, provenance). Check counts unchanged. - Remaining: the full-domain ACL collector (deepens both ADPATH-002 and this export) and cartography.
Transitive attack-path engine — chains control + group-membership edges to Tier-0.
- Transitive attack-path engine (
Resolve-AttackPathGraph+Get-ADTransitiveAttackPath). A directed privilege graph where every edge points "toward more privilege" (control + group-membership), with a BFS shortest-path resolver that chains edges of arbitrary length to Tier-0 — e.g.HelpDesk --[WriteDacl]--> CORP-Admins --[MemberOf]--> Domain Admins. Cycle-safe and depth-bounded. Builds on the existing default-principal exclusion (no v2.10.x false positives). - New check
ADPATH-002— Transitive Escalation Chains to Tier-0 (AttackPath category). Reports multi-hop chains (the single-hop case stays ADPATH-001's job), non-privileged sources first. AD is now 205 checks (473 total).
- Chain depth is bounded by ACL-collection coverage. Today's six-critical-object collection yields mostly one-hop edges, so ADPATH-002 is typically clean on current data; the full-domain ACL collector (live-gated, next increment) populates control edges over arbitrary objects and unlocks deep low-privilege-to-Domain-Admin chains. The engine itself is validated for arbitrary depth:
Tests/verify-transitive-attackpath.ps1(13/13) proves 3-hop chaining, shortest-path selection, cycle-safety, and depth bounding. - Still to come: the full-domain ACL collector, the BloodHound export, and cartography (visual domain/trust/attack-path map).
Maturity rating now lands in the board-facing report.
Export-ExecutiveSummarynow surfaces the Security Maturity rating. A color-coded Level X/5 badge in the stat row, plus a "Security Maturity" card with the overall level + label, the next-level blockers (what to fix to climb one level), and a per-category maturity table — computed viaGet-GuerrillaMaturityfrom the report's findings. This is a board-facing maturity artifact, and it's strict (worst-unmet-control anchors the rating).
- Report-only change; no new checks (counts unchanged). Validated: the section/badge/table render and are severity-color-coded (Level 1 = red through Level 5 = green); empty-findings path is safe.
- Remaining: cartography (visual domain/trust/attack-path map in the report) and the full-domain transitive attack-path graph + BloodHound export. Surfacing maturity in the per-theater Reconnaissance and Campaign reports is a small follow-on.
Maturity model — an executive-grade CMMI 1-5 maturity rating.
Get-GuerrillaMaturity— a CMMI-style 1–5 security maturity rating computed from audit findings (Active Directory, Google Workspace, or Entra/M365). Like a maturity model should, the worst unmet control anchors the score: a single open Critical caps the whole estate at Level 1 (Initial) regardless of how much else passes — stricter and more honest than an averaged 0–100 score, and the way an auditor/board reads posture.- Levels: 1 Initial → 2 Managed → 3 Defined → 4 Quantitatively Managed → 5 Optimized. Anchoring: FAIL caps by severity (Critical→1, High→2, Medium→3, Low→4), any WARN caps at 4; PASS/SKIP/ERROR never cap.
- Returns the overall level + label, per-category levels, the exact anchor findings holding you at the current level, and the next-level blockers (so advancement is concrete). Accepts pipeline input:
(Invoke-Reconnaissance).Findings | Get-GuerrillaMaturity -Theater ActiveDirectory.
- Next: surface this in the AD/Campaign reports as a maturity section + cartography (visual domain/trust/attack-path map), then the full-domain transitive attack-path graph + BloodHound export.
- 45 public functions now (was 44). Regression test:
Tests/verify-maturity.ps1(17/17). Check counts unchanged.
Live-validation fixes for the Adversary Tradecraft category.
- GTRADE-001 (DeleFriend) no longer reports a false PASS. There is no GA Directory API to list domain-wide-delegation grants (the legacy
/domainwidedelegationpath 404s on many tenants), so an empty collection means "could not enumerate," not "no grants." The check previously reported PASS "no grants configured" on emptiness — a false all-clear on the highest-value Google persistence vector. It now returns WARN with manual-verify guidance when grants can't be enumerated, and only PASS/FAIL when grants are actually present. (Also fixed the same empty→PASS masking in OAUTH-008.) Note:@($null).Countis 1, so the null case is now filtered explicitly. - GTRADE-005 (super-admin-equivalent custom roles) no longer over-matches read-only roles. The privilege matcher used guessed names; it now uses the real Google admin privilege vocabulary (
USERS_ALL/USERS_CREATE/USERS_RESET_PASSWORD/GROUPS_ALL/DOMAIN_MANAGEMENT/ORGANIZATION_UNITS_*/APP_ADMIN/ROLE_MANAGEMENT/MANAGE_*/SECURITY) and excludes read-only (_RETRIEVE) privileges, so a directory-reader role is no longer flagged as super-admin-equivalent.
- GTRADE-006 now labels OAuth grants with no friendly app name as
unnamed app (<client_id>)instead of surfacing a bare numeric/platform string, keeping the finding actionable.
- Live validation confirmed the v2.13.0 enum values (all booleans / a
"0s"duration) — existing grading is correct, no change needed. - GTRADE-002/003 (group exposure) remain pending live confirmation until the
apps.groups.settingsdomain-wide-delegation scope is delegated on the assessing service account; graceful degradation (collector →$null→ SKIP, scan completes) was confirmed live. - Counts unchanged (GWS 110 / AD 204 / Entra 158).
Tests/verify-gws-tradecraft.ps1now 24/24; test-mode dispatches 110 findings, 0 ERROR.
New Google Workspace Adversary Tradecraft category — detecting attack preconditions Google itself does not surface or alert on. GWS is now 110 checks across 9 categories (472 total).
- New GWS category: Adversary Tradecraft (
GoogleTradecraftChecks, 6 checks), the Google-Workspace analog of the AD Tier-0 / NTLM-relay-precondition checks. All read-only, weakest-OU/any-hit grading, graceful SKIP when data is unavailable:- GTRADE-001 — Domain-Wide Delegation org-takeover exposure (DeleFriend). Flags DWD grants holding org-impersonation scopes (full
mail.google.com, fulldrive,admin.directorywrite,cloud-platform,apps.groups) — each is a DeleFriend takeover target if its service account gets a new key. (Full confirmation — a user-managed key on the SA — needs GCP IAM, a Phase-2 scope; flagged in the finding.) - GTRADE-002 — Internet-readable Google Groups (
whoCanViewGroup = ANYONE_CAN_VIEW), the Kenna/UpGuard data-leak class Google doesn't alert on. - GTRADE-003 — Open-join / external-member groups (anyone-can-join or external members) — the open-group → IAM escalation precondition Google classifies "Won't Fix."
- GTRADE-004 — Super-admin sprawl (count vs. the <5 best practice).
- GTRADE-005 — Super-admin-equivalent custom roles (custom roles carrying user/security/role-management or data-export privileges).
- GTRADE-006 — Persistent / over-scoped OAuth grants (full mail/drive/admin scopes that bypass MFA and survive a password reset — GhostToken-class).
- GTRADE-001 — Domain-Wide Delegation org-takeover exposure (DeleFriend). Flags DWD grants holding org-impersonation scopes (full
- New collector
Get-GoogleGroupSettings— enriches directory groups with exposure settings (whoCanViewGroup/whoCanJoin/allowExternalMembers) via the Groups Settings API on the already-requestedapps.groups.settingsscope. Isolated token (graceful SKIP if undelegated); per-group, gated by-Quicklike the Gmail crawl; caps at 1000 groups and logs truncation (never silent). Wired intoGet-FortificationDataas$data.GroupSettings.
- Google Workspace is now 110 checks across 9 categories (was 104 / 8); module total 472 (was 466). Counts updated in README; the new category runs under
Invoke-Fortification(real + test mode) andInvoke-Campaign. - Phase-2 (deferred, needs a GCP IAM /
cloud-platformread scope + new collector): full DeleFriend confirmation (SA user-managed key × DWD), stale long-lived SA keys, and open-group→IAM-binding correlation. These can't be added without a new scope and live validation, so they're tracked, not shipped. - Regression test:
Tests/verify-gws-tradecraft.ps1(23/23). Test-mode Fortification dispatches all 110 findings with 0 ERROR.
Google Workspace coverage expansion — 6 net-new checks + ADMIN-008/009 converted. GWS is now 104 checks (466 total).
- 6 net-new Google Workspace security checks (all read live Cloud Identity policy, weakest-OU-wins, API-unavailable → SKIP):
- AUTH-014 (2SV Enrollment Allowed) →
security.two_step_verification_enrollment.allowEnrollment— WARN if users are blocked from enrolling in 2SV. - AUTH-015 (2SV Enrollment Grace Period) →
security.two_step_verification_grace_period.enrollmentGracePeriod— PASS ≤ 7 days, WARN if longer (longest-OU). - AUTH-016 (Advanced Protection Self-Enrollment) →
security.advanced_protection_program.enableAdvancedProtectionSelfEnrollment— PASS when high-risk users can self-enroll in APP. - AUTH-017 (Super Admin Account Self-Recovery) →
security.super_admin_account_recovery.enableAccountRecovery— FAIL if super-admin self-service recovery is on (account-takeover path). - COLLAB-011 (Meet External Participant Labeling) →
meet.safety_external_participants.enableExternalLabel— PASS when external participants are visibly labeled. - COLLAB-012 (Meet Host Management) →
meet.safety_host_management.enableHostManagement— PASS when hosts have moderation controls (mute/remove/lock).
- AUTH-014 (2SV Enrollment Allowed) →
- ADMIN-008 / ADMIN-009 converted from placeholders to real checks via
directory.workspace_resource_type_visibility(the onlydirectory.*policy type). ADMIN-008 (directory shared-contacts visibility) and ADMIN-009 (groups directory visibility) now read live config and WARN on broad directory exposure (audience-appropriate "review this," not FAIL), instead of an always-WARN "verify in Admin Console."
- Google Workspace is now 104 checks (was 98); module total 466 (was 460). Counts updated in README. 39 of the 104 GWS checks now read live Cloud Identity policy (33 conversions + 6 net-new).
- All net-new functions are dispatched and evaluate cleanly (test-mode Fortification: 104 findings, 0 ERROR). New regression suites:
Tests/verify-gws1-{auth,collab,admin}-p3.ps1.
Live-validation fixes: the Lookout baseline-persistence bug + confirmed-enum tighten-ups.
Invoke-Lookoutdrift detection was non-functional — baseline never persisted.Get-TheaterState/Save-TheaterStatecarried[ValidateSet('entra','ad','m365')], which rejected the'workspace'theater Lookout uses — so every run silently failed to save/load its baseline and re-baselined instead of detecting drift. Added'workspace'to the ValidateSet on both. New regressionTests/verify-lookout-state.ps1exercises the real state helpers (not mocks) across two runs and asserts the 2nd run loads the baseline (BaselineEstablished -eq $false) — the gap that let this ship (the prior Lookout test mocked the state helpers).
- Confirmed-enum tighten-ups (from live tenant values — closes WARNs that were grading unknown strings conservatively):
- COLLAB-008 (calendar external sharing): the real
maxAllowedExternalSharingfamily isEXTERNAL_*.EXTERNAL_ALL_INFO_*(shares full event details externally) now → FAIL;EXTERNAL_FREE_BUSY_ONLY/EXTERNAL_NO_SHARING→ PASS. - OAUTH-006 (
api_controls.app_approval_requests.allowedForAll): corrected interpretation. Per Google's Aug-2025 app-access-request-approval rollout,ENABLEDmeans the request-and-approve workflow is on (users request unconfigured apps for admin approval — access is not auto-granted), a governance positive → PASS (was mis-graded as "allowed for all = insecure"). The real app gate remains OAUTH-001/007. - OAUTH-001:
UNSPECIFIED_UBER_BLOCKconfirmed as block-all → PASS (made explicit; a bare not-set value still falls through to WARN).
- COLLAB-008 (calendar external sharing): the real
- EMAIL-019 remediation reworded ("Security > Data protection > Manage rules: …") so the evaluated WARN's guidance text no longer contains the placeholder phrase that tripped validation greps.
ADMIN-008/ADMIN-009(directory contact / profile sharing) are convertible viadirectory.workspace_resource_type_visibility(confirmed present in the full schema dump) — deferred to a follow-up pending secure-direction confirmation, to avoid shipping questionable grading.- Check counts unchanged (AD 204 / GWS 98 / Entra 158). All GWS-1 + Lookout suites green.
Google Workspace continuous monitoring — Invoke-Lookout closes the last gap in the GWS theater.
Invoke-Lookout— Google Workspace configuration-drift monitor. The GWS theater finally has a continuous-monitoring cmdlet to sit alongsideInvoke-Surveillance(Entra),Invoke-Watchtower(AD), andInvoke-Wiretap(M365). It runs the read-only Fortification posture audit, stores it as a baseline, and on each subsequent run diffs the current posture against the baseline — surfacing newly-failing controls (drift), resolved controls, and the posture-score change. It complementsInvoke-Recon(which watches user behaviour for compromise) by watching the tenant's configuration for regressions.- First run establishes the baseline (no drift reported); subsequent runs report the delta.
-Forcere-baselines.-ScanMode Fast(default) skips the slow per-user Gmail crawl (via Fortification-Quick);Fulldoes the complete sweep. - New failures are surfaced on the result's
.NewThreats, so it plugs straight into the alert wiring. Baseline state is stored under theaterworkspace. Built on the existingCompare-FortificationStateengine — no new collection.
- First run establishes the baseline (no drift reported); subsequent runs report the delta.
Register-Patrolnow schedulesInvoke-Lookoutfor the Workspace theater (alongsideInvoke-Recon), so a scheduled Workspace patrol covers both behavioural threats and configuration drift, each dispatching alerts whenSendAlertsis set.
- Read-only. Like the rest of the audit/monitor suite, Lookout makes no changes to Google Workspace — it only reads policy/config (the same collection
Invoke-Fortificationperforms) and writes local state/reports. (Verified: the only POSTs in the codebase are read queries — Graph$batch, Azure Policysummarize, Chrome Policyresolve— and there are no AD/Google write cmdlets.) - Exported cmdlet count is now 44 public functions (was 43). Check counts unchanged (AD 204 / GWS 98 / Entra 158).
- Regression test:
Tests/verify-lookout.ps1(16/16 — baseline, drift, resolved, no-findings guard, read-only call shape, Fast/Full-Quickhandling).
GWS-1 coverage extension — 7 more placeholders converted to real Cloud Identity policy checks (33 total).
- 7 additional Fortification checks now read live Cloud Identity policy instead of an always-WARN "verify in Admin Console":
- EMAIL-018 (Compliance Rules) →
gmail.content_compliance— PASS when ≥1 content-compliance rule is configured, else WARN. - EMAIL-019 (DLP Rules) →
rule.dlp— counts active, Gmail-scoped DLP rules (stateACTIVE+action.gmailAction); PASS if ≥1, else WARN. - DRIVE-010 (Drive DLP Rules) →
rule.dlp— counts active Drive-scoped rules (action.driveAction); PASS if ≥1, else WARN. (Gmail-only or inactive rules correctly don't count.) - ADMIN-010 (Groups External Membership) →
groups_for_business.groups_sharing.ownersCanAllowExternalMembers— FAIL if external members allowed in any OU (weakest-OU-wins). - ADMIN-011 (Group Creation Restrictions) →
groups_for_business.groups_sharing.createGroupsAccessLevel— FAIL on open creation, PASS on admin-restricted, WARN on unrecognized enum. - COLLAB-004 (Chat External Communication) →
chat.external_chat_restriction(policy-primary, with the existing OrgUnitPolicies path kept as fallback). - COLLAB-008 (Calendar External Sharing) →
calendar.primary_calendar_max_allowed_external_sharing(policy-primary, OrgUnitPolicies fallback retained).
- EMAIL-018 (Compliance Rules) →
- GWS-1 coverage is now 33 real policy-backed checks (was 26 in v2.11.0). Check counts unchanged (AD 204 / GWS 98 / Entra 158) — logic changed, not the check set.
- Same safety rails as v2.11.0: weakest-OU-wins grading, API-unavailable→SKIP vs policy-absent→SKIP, and unrecognized enums grade WARN, never PASS (DLP/state matching is anchored so
INACTIVEnever counts as active). Enum strings for ADMIN-011, COLLAB-004/008 remain best-effort pending live confirmation. - Remaining placeholders with no Cloud Identity Policy API equivalent stay documented manual-verify (TLS, inbound gateway, SSO, app passwords, unverified-apps, appointment slots, ownership transfer). The MDM/device family and directory/profile-sharing checks are pending the full 173-type schema dump.
- New regression suites (all green):
Tests/verify-gws1-{email,drive,admin,collab}-p2.ps1; existing suites unchanged.
GWS-1 complete — the Cloud Identity policy data layer (v2.10.8) is now wired into real checks.
- GWS-1: ~60 "verify in Admin Console" placeholders converted to real checks (26 now evaluate live policy). Building on the v2.10.8
Get-GoogleCloudIdentityPoliciescollector, the Fortification placeholder checks that map to a Cloud Identity policy setting now read it and return real PASS/FAIL/WARN instead of an always-WARN "verify manually". Converted:- Authentication (6): AUTH-003 (2SV method strength), AUTH-004 (password min length), AUTH-005 (password reuse), AUTH-006 (web session duration), AUTH-008 (less-secure apps), AUTH-011 (login challenges).
- Email Security (6): EMAIL-013 (pre-delivery scanning), EMAIL-015 (attachment safety), EMAIL-016 (link/image scanning), EMAIL-017 (spoofing/authentication), EMAIL-020 (confidential mode), EMAIL-021 (S/MIME cert upload).
- Collaboration (5): COLLAB-001 (Meet recording), COLLAB-002 (Meet audience), COLLAB-003 (Meet anonymous join), COLLAB-005 (Chat history), COLLAB-006 (Chat external spaces).
- Drive (3): DRIVE-001 (external sharing mode), DRIVE-004 (shared-drive creation), DRIVE-008 (Drive for Desktop).
- OAuth (3): OAUTH-001 (third-party app access), OAUTH-006 (API access control), OAUTH-007 (Marketplace app installs).
- Logging/Alerting (2): LOG-004 (cloud data sharing/export), LOG-005 (admin alert rules active).
- Admin (1): ADMIN-012 (Groups for Business service status).
- New shape-immune helper
Resolve-GooglePolicyValue(+ConvertFrom-GoogleDurationSeconds). It normalizes the policy lookup so checks are immune to whetherGet-GooglePolicySettinghands back value objects or policy objects, returns per-OU field values, and distinguishes API-unavailable (→ SKIP) from type-absent (→ SKIP/PASS) — fixing areturn @()→$nullunwrap that would otherwise mislabel "policy absent" as "API unavailable". Grading is weakest-OU-wins (min length / longest session / any-insecure boolean).
- Checks with no Cloud Identity Policy API equivalent remain documented manual-verify (honest coverage, not forced mappings): most Email routing/compliance (EMAIL-005/006/007/008/014/018/019), several Drive sub-settings, Calendar/Chat-app items, OAuth unverified-app/service-account-key, Admin directory/profile/group-creation, and all mobile-device/MDM checks (DEVICE-002..010) — the policy API doesn't expose them.
- Enum caveats for live confirmation: AUTH-003 (
allowedSignInFactorSet), DRIVE-001 (externalSharingMode), COLLAB-002/003 (Meet audience enums), OAUTH-001/006/007 (access-level enums) grade known-insecure values as FAIL and anything unrecognized as WARN — never PASS on an unknown enum, so a different enum spelling degrades safely. Exact strings should be confirmed against the liveraw/gws-policy-schemas.txt. - Check counts unchanged (AD 204 / GWS 98 / Entra 158 = 460) — conversions changed check logic, not the check set.
- Regression tests (all green):
Tests/verify-gws1-auth-checks.ps1(20),-email-(15),-collab-(17),-drive-(11),-oauth-(16),-admin-(12),-logging-(10),-device-(4), plus the existing-policy-collector(8).
- GWS-1 enabling infrastructure — Cloud Identity Policy collector. New
Get-GoogleCloudIdentityPoliciescollector pulls the full Workspace settings set from the Cloud Identity Policy API (policies.list, paginated) and indexes it by setting type, plus aGet-GooglePolicySettinglookup helper; wired intoGet-FortificationDataasCloudIdentityPolicies. This is the data layer that turns the ~60 "verify in Admin Console" placeholder checks (Gmail / Drive / Auth / Chat / Meet / Calendar / DLP / service-status) into real checks — the check conversions come next, once the livesetting.valueshapes are confirmed. Thecloud-identity.policies.readonlyscope is requested in an isolated token so a tenant that hasn't delegated it degrades gracefully (collector returns$null, dependent checks SKIP) instead of breaking the whole Google scan withunauthorized_client.
- Chrome-policy collection no longer hardcodes a tenant-specific org-unit id.
Get-FortificationDataresolved Chrome policies against a hardcodedorgunits/<id>— which only worked for one tenant (a bug for everyone else) and embedded a tenant identifier. It now resolves the customer's root org-unit id dynamically (from the directory API) and skips gracefully if it can't.
- Teams
/appCatalogs/teamsAppsneeds no change: the collector already queries with$filter(not$top), andInvoke-GraphApipaginates via@odata.nextLink— so the$top-rejection the validation flagged doesn't apply to our call path. - Regression test:
Tests/verify-gws1-policy-collector.ps1(8/8 — indexing, lookup, graceful degradation). Check counts unchanged.
- Spectre.Console console rendering restored (no more
AddItem/AddRow/BorderColorerrors). WhenPwshSpectreConsoleis installed, the bar charts and tables were calling Spectre.Console C# extension methods as instance methods ($chart.AddItem(…),$table.AddRow(…),$table.BorderColor(…),$tree.AddNode(…)) — which PowerShell can't do, so every non--Quietscan spammed "does not contain a method named …" and the chart/table/tree came out blank. They now call the correct static extension classes (BarChartExtensions::AddItem,TableExtensions::AddRow,HasTreeNodeExtensions::AddNode) and set the border viaBorderStyle, so the category bar charts, "Findings by severity" chart, and the Priority-findings table now render in the console. Each enhanced renderer is also wrapped in a try/catch that falls back to the text renderer, so a future Spectre.Console API change degrades gracefully instead of spamming. - Test mode now uses a zeroed report-filename timestamp. With
-TestModethe CSV/HTML/JSON reports are written as…_report_00000000_000000.…(Reconnaissance / Fortification) and…-00000000-000000.…(Infiltration / Campaign) instead of the live clock, so demo/sample output is fully deterministic — completing the test-mode determinism started in v2.10.5. (Real scans keep the real timestamp.)
- Defender / EDR false-positive guidance (the most common first-run failure). Guerrilla's AD attack-detection files (DCSync GUIDs,
GenericAll/WriteDacl, shadow-admin, Tier-0 patterns) can trip antivirus heuristics — Microsoft Defender real-time protection in particular blocks read access to them, soImport-Modulefails with "Access to the path '…Invoke-ADAclDelegationChecks.ps1' is denied" (often a different AD file each attempt). README now documents this prominently in Requirements with theAdd-MpPreference -ExclusionPathfix and a Protection-history "Allow" alternative, plus a dedicated Troubleshooting section. Surfaced by the v2.10.4 live validation. - Added
AppCatalog.Read.Allto the documented Entra app-registration scopes (the Teams app-catalog collection calls/appCatalogs/teamsApps; without the scope that portion logs a handled 403 and stays empty). Added Troubleshooting entries for that 403 and for the "No accessible Azure subscriptions" SKIP.
- Test mode now renders deterministic (zeroed) timestamps. When a scan runs with
-TestMode(or the GUI "Test mode" checkbox), all console timestamps read00:00/00:00:00instead of the live clock, so demo / sample / screenshot output is stable: the operation header shows… 0000 UTC(date kept real),Write-ProgressLineshows[0000 UTC], and the GUI Operations log prefixes each line with[00:00:00]. Driven by a self-healing module flag ($script:GuerrillaTestMode) the audit cmdlets set per run; a real (non-test) run resets it. Report filenames still use the real timestamp (zeroing them would collide).
Backlog sweep — the code-only gaps that don't need a live tenant/DC to build and verify.
- (GUI-1) Real "Add Credential" modal. The Safehouse tab's "Add Credential" button now opens a dark-themed WPF dialog (not a redirect-to-terminal stub) that stores Microsoft Entra / Graph (tenant / client / secret + optional expiry) or Google Workspace (service-account JSON via file picker + delegated-admin email) credentials straight into the vault, then refreshes the grid. Field validation (GUID / email / valid SA JSON) runs before anything is written. Backed by a non-interactive
Save-SafehouseCredentialSethelper; the dialog's builder/validator are pure and unit-tested, and the window was render-verified off-screen.
- (ENT-5) Azure IAM now distinguishes "no ARM access" from "no resources." Every
AZIAM-*resource check shared one guard that emitted a misleadingWARN: No X found in scanned subscriptionseven when the real problem was zero Azure access. A sharedGet-AzureIAMUnavailableFindingnow returns a single clear SKIP — "No accessible Azure subscriptions — grant the app the Reader role at the root management group" (or surfaces the ARM authorization error) — so the WARN only fires when subscriptions exist but genuinely have no resources of that type. - (ENT-4, partial) Consolidated the M365 workload-skip noise. Instead of ~40 individual
SKIP: <workload> not connectedlines,Invoke-Infiltrationnow prints one pre-flight banner summarizing which workload modules (EXO / Teams / SharePoint / Power Platform) are not connected and how many checks each skipped. (Net-new workload checks still need live Graph/admin-module validation and remain on the roadmap.) - (DSInternals) Single pre-flight note. When DSInternals isn't installed, reconnaissance prints one note that the 5 password-hash checks (
ADPWD-010..014) will SKIP, instead of five identical per-check skip lines.
- Regression tests:
Tests/verify-ent5-azure-skip.ps1(7/7),Tests/verify-gui-credential-entry.ps1(15/15). AD/GWS/Entra check counts unchanged (204 / 98 / 158). - Still gated on a live environment (can't build+validate from a dev box), unchanged: full-domain transitive attack paths, GWS-1 Cloud Identity Policy API (blocked on the DWD scope), GWS-3 full parallelization, ENT-4 net-new workload checks, ADDOM-007 replication health, and the security-event-log behavioral ITDR layer.
Fixes from the v2.10.1 attack-path validation and the v2.10.2 GUI re-check.
- ADPATH-001 false positives eliminated. The attack-path engine was reporting default infrastructure/admin principals (the
Domain Controllersgroup,Enterprise Domain Controllers, RODCs,Enterprise Read-only Domain Controllers,Schema Admins) as Tier-0 escalation paths — they legitimately hold replication/control rights by AD design. A new centralized allowlist (Test-DefaultControlPrincipal, matched by forge-proof well-known SID/RID, not locale-dependent names) excludes them. In the reporter's live domain this drops the headline from "32 paths, 32 non-privileged" to the ~7 genuine ones. SourceIsPrivilegednow correct. Previously every path was flagged non-privileged (so the "highest-risk" sort/count was meaningless). It now returns true for default privileged principals (incl. the operator groups) and Tier-0 group members, so the non-privileged bucket contains only genuinely non-privileged custom principals.- Azure AD Connect sync accounts (
MSOL_*) relabeled. They hold real DCSync rights but by design (and are already tracked byADTIER-001), so they're flaggedExpected, kept out of the non-privileged count, and reported separately ("plus N expected service-account path(s) — see ADTIER-001") instead of as surprise escalations. - DCSync / ACL-delegation checks now share the same allowlist.
Test-SafeAdminSid(used byADPRIV-028,ADACL-010/015/016, …) delegates toTest-DefaultControlPrincipal— a strict superset of its old list. This closes the v2.10.1 residual where Enterprise Read-only Domain Controllers (498) was reported as a non-default DCSync principal, and keeps the DCSync/ACL checks consistent with the attack-path engine. (Re-validate the ADACL/ADPRIV non-default counts — they may drop slightly as more by-design infra principals are correctly excluded.) - (GUI-2) ComboBox selection box actually fixed this time. The closed box rendered with the system's light button chrome (because the nested
ToggleButton's{TemplateBinding Background}bound to the unset ToggleButton background, not the ComboBox's), so near-white selection text read as blank/faint. The box fill is now hardcoded dark — verified by rendering the control off-screen to a bitmap and inspecting the pixels (Tests/tools/render-combo-probe.ps1), not by eye.
- Regression tests added:
Tests/verify-adpath-fix.ps1(19/19 — default-principal exclusion incl. a localized-name-but-RID-516 case, MSOL relabeling, genuine-path retention, headline correctness). - Roadmap unchanged: full domain-wide transitive path chaining still needs the full-domain ACL collector (next increment). The reporter also noted
ADPATH-001object-control paths overlapADACL-016; a future pass may cross-reference/dedupe them so the same ACE isn't counted by multiple checks.
GUI + Safehouse fixes from the live GUI/Safehouse validation pass.
- (SH-1) Config-file setup now persists the Google Workspace admin email to the vault.
Set-Safehouse -ConfigFilemigrated the service-account JSON but never storedGUERRILLA_GWS_SA_ADMIN_EMAIL, so a config-file setup followed by a vault-only scan (GUI / scheduled patrol, no-ConfigFile) failed with "AdminEmail is required." The migration now also stores the admin email (fromgoogle.adminEmail). - (SH-4) Config migration now also handles Pushover and Twilio/SMS providers (previously silently dropped — only teams/slack/sendgrid/mailgun/pagerduty were migrated), using canonical keys
GUERRILLA_PUSHOVER_KEY/GUERRILLA_TWILIO_KEY. - (SH-2) Status surfaces reconcile metadata with the real secret store.
Get-Safehouse,Set-Safehouse -Status, and the GUI Safehouse tab were metadata-driven (GUERRILLA_VAULT_METADATA), so secrets stored without a metadata entry (the interactive admin-email write, Pushover, a legacy bare key) were invisible — a "are my creds loaded?" blind spot. A newGet-SafehouseCredentialViewhelper reconciles metadata withGet-SecretInfo, surfacing present-but-unregistered keys (flaggedunregistered). - (SH-3) Vault status discloses the no-master-password mode. The store is configured with
Authentication Nonefor unattended runs; status now plainly states "DPAPI at rest, no master password — any process running as this user can read these secrets" instead of just labeling itDPAPI. - (GUI-1) The Safehouse "Test All" button now works. It runs the real
Test-Safehouseconnectivity engine asynchronously (off the UI thread) and shows per-environment, per-check results, instead of redirecting to the terminal. - (GUI-2) ComboBoxes show their selected value when collapsed. Report style / Profile / Minimum alert level rendered blank when closed because the stock WPF template themed the selection box via the system theme; a full dark
ControlTemplatenow themes theSelectionBoxItem. - (GUI-4) Single-instance guard. A named mutex stops a second
Show-Guerrillawindow from opening and clobbering the sharedconfig.json/*-state.json(last-writer-wins); the second launch warns and exits. - (GUI-5) Rotate/Remove give feedback when no row is selected (previously a silent no-op).
- GUI-3 (Safehouse tab under-reporting) is resolved by the SH-2 fix (the tab reads
Get-Safehouse). GUI-6 (Patrol scheduling broken monitoring) was resolved by the v2.9.4 MON-4 fix. - Full GUI-driven credential entry ("Add Credential") remains a roadmap item; the terminal
Set-Safehouseflow is still the entry path. - Regression tests added:
Tests/verify-safehouse-fixes.ps1(10/10 — SH-1/SH-2/SH-4).
- Attack-path analysis now flags group-nesting pivots. Building on v2.10.0's object-control paths,
ADPATH-001also reports non-default groups nested inside a Tier-0 group (Domain / Enterprise / Schema Admins, Administrators, the operator groups) as escalation pivots — controlling such a group, or being added to it, confers the Tier-0 group's privileges. Uses the already-collected recursive privileged-group membership (no new collection); the well-known Tier-0 groups themselves are excluded so only custom nesting is flagged. Each path now carries aPathType(Object control/Group nesting).
- AD attack-path analysis (
ADPATH-001, new "AttackPath" category).Invoke-Reconnaissancenow turns the flat dangerous-ACL findings into named privilege-escalation paths to Tier-0, each annotated with the concrete takeover technique it enables (e.g. "CORP\HelpDesk --[WriteDacl]--> Domain Root ⇒ can grant themselves DCSync replication rights and extract every domain hash — Domain Admin equivalent"). v1 models the highest-value edge class — non-default control (GenericAll / WriteDacl / WriteOwner / replication rights) over a Tier-0 object (the domain root, AdminSDHolder, the Domain Controllers OU, the GPO / Configuration / Schema containers) — which is a one-hop path to Domain Admin equivalence. Paths from genuinely non-privileged principals are surfaced first as the highest risk. Built entirely on the already-collected ACL + privileged-group data (no new collection); runs under-Categories AllorACLDelegation/AttackPath. AD coverage is now 204 checks across 15 categories (460 total).
- This is the first increment of the roadmap's headline gap (graph-based attack-path computation). Full domain-wide transitive path computation (low-priv user → nested-group control → Domain Admins) requires a full-domain ACL collector, which Guerrilla does not yet run (it reads ACLs on the 6 critical objects only); that deeper traversal is the next step, and the engine (
Get-ADAttackPath) is structured to take additional edge sources directly. - Regression tests added to
Tests/verify-core-fixes.ps1(the engine derives paths, flags non-privileged sources, and the check returns FAIL/PASS/SKIP correctly).
From the v2.9.3 live re-validation.
- MON-4 (regression) — continuous monitoring broke after the first run.
Invoke-SurveillanceandInvoke-Wiretapsucceeded once and then threw "Item has already been added" on every subsequent run, silently killingRegister-Patrolscheduled monitoring. The scan-history append used@($state.scanHistory) += @{...}, which — once a prior single-entry history reloaded from JSON — performed a hashtable-key merge and threw. Both cmdlets now build history via a newAdd-ScanHistoryEntryhelper (List-based, tolerant of a collapsed single-object history) that always returns a clean array. A two-run regression test was added (the exact case that was missing). (MON-4) ADPRIV-028(DCSync rights) now reports instead of always SKIPping. With the AD-1 ACL fix in place the domain-root DACL is collected, butADPRIV-028read aDCSyncAccountsfield nothing ever populated. The collector now derivesDCSyncAccountsfrom the dangerous-ACE set (filtering the replication extended-right GUIDs1131f6aa/1131f6ad/89e95b76and dropping default Tier-0 principals), soADPRIV-028lights up — completing the DCSync attack-path coverage that AD-1 unblocked. (AD-1b)
Invoke-Fortification -Quick— skips the slow per-user Gmail-settings crawl (which dominates wall-clock on large tenants: ~1.4 s/user, ~11 min for 500 users). Directory, DNS, and OAuth collection still run; the Gmail-dependent EMAIL checks SKIP cleanly. For fast iteration. (GWS-3, partial)
- The full GWS-3 fix (parallelizing the per-user crawl with
ForEach-Object -Parallel) is deferred: it needs care with module-function/token availability inside parallel runspaces and live-tenant validation, and getting it wrong would risk the core exfil-detection checks. The-Quickprofile is the safe mitigation for now. - GWS-2b (labeling sampled-clean results "SAMPLED N of M") already shipped in v2.9.3 via
Get-GmailSampleNote. Still open from the re-validation: GWS-1 (Cloud Identity Policy API — blocked on thecloud-identity.policies.readonlyDWD scope), ENT-4 (M365 workload coverage), ENT-5 (Azure IAM messaging).
Get-ComplianceCrosswalknow surfaces the technical frameworks already carried on every check. AddedNIST-800-53,MITRE-ATTACK, andCISto-Framework, built directly from each finding'sCompliancemap (NIST SP 800-53 controls, MITRE ATT&CK techniques, and CIS benchmarks including CIS-AD / CIS-M365 / CIS-Azure). Previously only the education frameworks (FERPA / COPPA / CIPA / NIST-171 / STATE-EDTECH) were exposed even though the richer mappings were already collected. (REP-2)
- Sampled Google Workspace Gmail checks no longer overstate coverage.
EMAIL-009/010/011/022(auto-forwarding, send-as, POP/IMAP, forwarding rules) now append a "SAMPLED N of M active mailboxes" qualifier to a clean PASS when only a subset of mailboxes was inspected — so a partial scan can't read as full coverage. Pairs with the random-sampling fix from v2.9.2. (GWS-2) ADTRADE-002(DCShadow indicator) softened from Critical to High. On long-lived domains an unmatched server object underCN=Sites,CN=Configurationis far more often lingering DC metadata (a DC removed withoutntdsutilmetadata cleanup) than an actual DCShadow attack; the finding now says so and points at thewhenCreatedtimestamp to distinguish a recently created (suspicious) object from stale metadata. (ADTRADE-002)
- Quieter Entra scans on tenants without P2.
Invoke-GraphApinow treats license-gated HTTP 400s (AadPremiumLicenseRequired, e.g. the PIM schedule-instance endpoints) as aWrite-Verbosecapability-gap note instead of an alarming redWrite-Warning. (ENT-3)
- Remaining backlog (tracked, not yet done): GWS-1 (convert the ~60 Google Workspace "verify in Admin Console" placeholders to real checks via the Cloud Identity Policy API — blocked until the service account's domain-wide delegation is granted
cloud-identity.policies.readonly; adding that scope to the requested set before it is delegated would break all Google auth withunauthorized_client, so this must wait for the scope + live validation); ENT-4 (app-only Graph coverage for M365 workloads / opt-in EXO + Teams modules); ENT-5 (Azure IAM "no ARM access" vs "no resources" messaging — the safe approach is a zero-subscriptions guard that SKIPs with a "grant Reader at the root management group" message); GWS-3 (parallelize Fortification's per-user collection); andADDOM-007replication health (needs a live DC + RSAT/repadminto validate).
Part 2 of the live-environment validation pass — Google Workspace, continuous monitoring, and reporting.
- Continuous monitoring couldn't use the safehouse vault.
Invoke-SurveillanceandInvoke-Wiretapnever read the vault, so a vault-only setup (interactiveSet-Safehouse, noguerrilla-config.json) failed immediately with "TenantId is required" — even thoughInvoke-Infiltrationhandled the same vault fine, and this brokeRegister-Patrolscheduled monitoring for vault installs. Both cmdlets now have a-VaultNameparameter and resolveTenantId/ClientId/ClientSecretfrom the vault (GUERRILLA_GRAPH_*) as the last resort after parameters and config — the same fallback the audit cmdlets got in v2.5.0. Invoke-Surveillanceaborted the entire run on the first Graph 403. Its collectors had no isolation, so a missing Identity-Protection scope (or no Entra ID P2) killed all monitoring. Each collector is now wrapped intry/catch; the risk-detection403/AadPremiumLicenseRequiredcase degrades to a clear "requires IdentityRiskEvent.Read.All + IdentityRiskyUser.Read.All scopes and an Entra ID P2 license" skip, and the sign-in / audit-log signals still run.- Google Workspace Gmail sampling was non-random.
Get-FortificationDataselected mailboxes withSelect-Object -First, always inspecting the same directory-order prefix (often skewed to a single OU) — so a compromised mailbox later in the list was never examined and a "clean" sampled result gave false assurance. Now uses a random sample (Get-Random). Export-RemediationScripts -OutputPathnow works. It was the onlyExport-*cmdlet using-OutputDirectory; added-OutputPathas an alias for parity with the other exporters.Invoke-Watchtowergained comment-based help (.SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE) —Get-Helppreviously returned only auto-generated syntax.
- Larger improvements from the same report are tracked as follow-ups: converting the ~60 Google Workspace "verify in Admin Console" always-WARN placeholders into real checks via the Cloud Identity Policy API (GWS-1); parallelizing Fortification's per-user collection (GWS-3); and expanding
Get-ComplianceCrosswalkto surface the NIST 800-53 / MITRE ATT&CK / CIS mappings already present in every check (REP-2).
Surfaced by a live-environment validation pass against a production Active Directory domain and Entra tenant.
- AD ACL / DCSync / GPO-delegation checks were silently dead.
Resolve-ADSidreferenced three module-scope caches ($script:SidCache,$script:WellKnownSids,$script:WellKnownRids) that the.psm1bootstrap never initialized — so every ACL read threw "You cannot call a method on a null-valued expression",Get-ADObjectACLsswallowed it, andADACL-012/ADACL-014,ADGPO-007/ADGPO-009, and the DCSync-rights checkADPRIV-028all became false SKIPs. The caches are now initialized at load (with the well-known SID/RID tables), with a belt-and-suspenders guard inResolve-ADSid; the domain-RID lookup is gated onS-1-5-21-*SIDs to avoid collisions. - Tier-0 "tier bleed" checks ERRORed on clean environments.
ADTIER-002/003/004/005(Veeam/vCenter/SCCM/SQL service accounts in privileged groups) calledNew-TierBleedFinding, whose[Parameter(Mandatory)][array]$Hitsrejected an empty collection — so the secure state (zero hits) threw instead of returning PASS. Added[AllowEmptyCollection()]. - Entra password-protection checks falsely reported "settings not found".
Get-EntraAuthMethodsDataqueried/settings(a beta-only segment that 400s on v1.0); it now uses the v1.0/groupSettingsresource, soEIDAUTH-013/EIDAUTH-014read the real Azure AD Password Protection posture. - Removed a redundant, always-400 Graph call. The standalone
.../authenticationMethodsPolicy/authenticationMethodConfigurationsendpoint isn't directly addressable; the configurations are now sourced from the parentauthenticationMethodsPolicyobject (which already includes them). - Quieter logs on healthy domains.
Invoke-LdapQuerynow treats "no such object" (e.g. a domain with no AD CS / Enterprise CA, or an empty DNS partition) asWrite-Verbose+ empty result instead of an alarmingWrite-Warning. The dependent check still SKIPs the same way.
- Added
Tests/verify-core-fixes.ps1covering theResolve-ADSidandNew-TierBleedFindingregressions. - Larger improvements from the same validation report — license-gated PIM checks surfacing raw HTTP 400s instead of clean "requires Entra ID P2" SKIPs, app-only Graph coverage for M365 workloads (opt-in EXO/Teams/SharePoint), and Azure IAM "no access vs. no resources" messaging — are tracked as follow-ups.
- Test mode — a
-TestModeswitch onInvoke-Reconnaissance,Invoke-Fortification,Invoke-Infiltration, andInvoke-Campaign, plus a "Test mode" checkbox in theShow-GuerrillaOperations tab. When enabled, the scan makes no live connection and instead synthesizes a complete all-FAIL report straight from the shipped check definitions. Everything downstream behaves exactly like a real scan: report styles/themes, white-label branding, affected-account lists, scoring, and CSV/JSON output. This lets a consultant preview a fully-populated report — and dial in branding/theme — without a tenant or domain. Works for all three theaters and the big Campaign report (which simulates all 459 checks across AD + Google Workspace + Entra/M365).
- The Campaign (big report) now honours report themes and white-label branding too:
Invoke-Campaigngains-ReportStyle, reads branding from config, andExport-CampaignReportHtmlwas moved onto the shared theming engine (Guerrilla / Professional / Slate), including plain risk-based per-theater labels in the plain themes. (Previously only the three single-theater reports were themed.)
- In test mode the category selection is ignored — a full theater check set is always simulated (the point is a "fully failed report"). Real scans are unaffected.
- Backward compatible: default Guerrilla output is unchanged and all 64 HTML report validation checks still pass.
- Entra / Azure / M365 (Infiltration) scans launched from the GUI appeared to hang. The scan actually completed, but the GUI's
OnCompletecallback called the module-privateGet-GuerrillaDataRoot, which isn't resolvable inside aGetNewClosure()closure — so the callback threw before resetting the UI, leaving the progress bar spinning on "still working…". The AD and Google Workspace paths were unaffected because their result objects carryHtmlReportPathand skipped that branch. Two fixes: the callback now uses the already-captured$session.ReportsDirinstead of the private function, andInvoke-Infiltrationnow returnsHtmlReportPath(like the other theaters) so the GUI opens the exact report. - Poor contrast in the GUI dropdowns and left navigation. The "Report style" (and Settings) dropdown popups used WPF's default light system theme, rendering the near-white item text invisible.
ComboBoxItemnow has an explicit dark control template (dark background + light text, with an amber highlight + dark text on hover). The left-nav button text was also dimmed and has been brightened for legibility.
-
Report themes / styles. Reports can be generated in three visual styles, selectable per scan from the Operations tab's new "Report style" dropdown (and via a new
-ReportStyleparameter onInvoke-Reconnaissance,Invoke-Fortification, andInvoke-Infiltration):- Guerrilla (default, unchanged) — the original dark, tactical theme with FORTRESS / EXPOSED FLANK / OVERRUN posture labels.
- Professional — a light, white-background corporate theme with a sans-serif body and plain risk-based labels (Secure / Hardened / Moderate Risk / Elevated Risk / High Risk / Critical Risk).
- Slate — a modern dark dashboard theme, also with plain risk-based labels.
A new theming engine (
Get-GuerrillaReportTheme) drives a shared palette of CSS custom properties, so all three audit reports (AD / Google Workspace / Entra-M365) share one consistent look per style. -
White-label branding. A new "Branding" tab in
Show-Guerrillacaptures firm name, logo (file path or URL), consultant name + email, client / assessed-org name, and a confidentiality banner. These render in the report header (firm + logo, "Prepared by", "Prepared for") with the confidentiality banner across the top. Branding is saved to your config and applied to every subsequent scan. The "Generated with Guerrilla by Jim Tyler, Microsoft MVP" footer attribution is always preserved regardless of theme or branding. -
Showcase sample reports in the Professional theme with demo branding:
Samples/*-AllFail-Professional.html.
- The default look is unchanged — existing scans render identically (Guerrilla) unless a style is chosen. Backward compatible: all 64 HTML report validation checks still pass.
- Themes + branding currently apply to the three audit reports (Reconnaissance / Fortification / Infiltration). The Campaign roll-up and continuous-monitoring reports keep the Guerrilla styling for now.
Show-GuerrillaGUI: new "Inspector" tab — a built-in function & scan source browser. Lists every function in the module (801 today), filterable by area (Active Directory, Google Workspace, Entra / Azure / M365, Monitoring, Reporting & Export, Public cmdlets, Core & helpers, GUI) and searchable by name. Selecting a function shows its full source, the file path and line number it lives at, and a Copy button — so an operator can confirm exactly what any scan/check is doing without leaving the console or opening the repo.Show-Guerrilla -StartOn Sourceopens directly to it.
- Google Workspace scans now leave "Email Security" unchecked by default in the GUI's category picker — it's opt-in (the noisier, slower set). Every other Workspace category stays checked, and the "All" toggle reflects the partial selection. Active Directory and Entra / M365 category defaults are unchanged (all checked).
- The Google Workspace (Fortification) report is now far more actionable, with the Authentication category built out as the first pass:
- Affected accounts are now listed. When a check flags a problem against specific accounts — users without 2SV enforced/enrolled, super admins not enrolled in 2SV, stale super admins, super admins with personal recovery options — the HTML report now lists the actual affected accounts beneath the finding (capped at 25 with a "+N more" indicator) instead of showing only a count. The renderer also auto-surfaces affected-object lists that existing checks already capture (e.g.
ActiveSuperAdmins), so other categories benefit immediately. - "Fix in Admin Console" deep-links on every finding. The Admin console deep-link previously appeared only in the Critical/High priority table; it now appears on every actionable finding in the per-category detail tables.
- "Why this is unsafe" reference articles. Each Authentication check now links to an authoritative article explaining why the misconfiguration is dangerous — Google Workspace official documentation where available, supplemented by NIST/CIS/MITRE and reputable security research where they explain the attack better. New
referenceUrl/referenceTitlefields on check definitions; all 13 URLs were verified to resolve (HTTP 200).
- Affected accounts are now listed. When a check flags a problem against specific accounts — users without 2SV enforced/enrolled, super admins not enrolled in 2SV, stale super admins, super admins with personal recovery options — the HTML report now lists the actual affected accounts beneath the finding (capped at 25 with a "+N more" indicator) instead of showing only a count. The renderer also auto-surfaces affected-object lists that existing checks already capture (e.g.
- All three HTML report footers showed "v2.0.0" regardless of the installed version. The version-detection logic walked
$PSScriptRootup one level too many (threeSplit-Path -Parentcalls fromPrivate/Export/overshot the module root), so the manifest was never found and every report fell back to the hardcoded default. All three exporters (Fortification, Reconnaissance, Infiltration) now read the version directly from the running module via$ExecutionContext.SessionState.Module.Version.
- This is the first category in a rollout. The rendering improvements (affected-account lists, admin-console links on every finding) apply to all Google Workspace categories now. The curated reference articles currently cover the 13 Authentication checks and will be extended to the remaining seven categories in follow-up releases.
- New finding properties
ReferenceUrl/ReferenceTitleflow throughNew-AuditFinding; the affected-account convention isDetails.AffectedItems+Details.AffectedLabel. Both are backward compatible — all 64 existing HTML report validation checks still pass.
- The sample-report generator undercounted Active Directory checks as 175 instead of the real 203.
Samples/Generate-SampleReports.ps1built its AD report from a hardcoded$adFileslist of only 10 of the 14 AD check-definition files, silently omittingADLoggingChecks(7),ADNetworkChecks(10),ADTradecraftChecks(4) andTierZeroChecks(7) — the 28 checks added in v2.2.0. The generator now discovers every AD check file automatically (a case-sensitive match on theAD/TierZeroprefix, so the Google WorkspaceAdminManagementChecks.json— lowercased— is never captured), so newly added categories can't silently drop out again. - Regenerated the committed
Samples/Reconnaissance-AllFail.html, which now reflects all 203 AD checks (GWS 98 + AD 203 + Entra 158 = 459 total).
- This was purely a sample/count bug. All 203 AD checks were always implemented and run by
Invoke-Reconnaissance— verified as a 1:1 mapping between the 203 JSON check IDs and the 203Test-Recon*dispatch functions, with zero stubs. The module's advertised "203 AD checks / 459 total" was already correct.
- The Active Directory report's "Findings by Priority" table now includes a Remediation column. Previously it showed only ID / Severity / Status / Category / Check / Finding, and remediation guidance lived only in the per-category detail tables further down. It falls back to the recommended value when a check has no explicit remediation steps. (Google Workspace, Entra/M365, and Campaign reports already surfaced remediation in their findings tables / detail rows.)
- Regenerated the committed sample reports under
Samples/to reflect the new column.
- Scans auto-resolve credentials from the safehouse vault. Previously
Invoke-Fortification,Invoke-Infiltration, andInvoke-Campaignonly read the vault when handed a-ConfigFile(guerrilla-config.json) mission file — so an interactiveSet-Safehousesetup (no config file) couldn't scan at all, failing withServiceAccountKeyPath is required/TenantId is required, including from theShow-GuerrillaGUI. These cmdlets now fall back — as a last resort, after explicit parameters andconfig.json— to the default vault keysSet-Safehousestores:GUERRILLA_GWS_SA(+_ADMIN_EMAIL) for Google Workspace andGUERRILLA_GRAPH_TENANT/GUERRILLA_GRAPH_CLIENTID/GUERRILLA_GRAPH_SECRETfor Entra/Azure/M365. A populated safehouse now "just works" for every theater from both the CLI and the GUI. -VaultNameparameter (defaultGuerrilla) onInvoke-Fortification/Invoke-Infiltration/Invoke-Campaign, so non-default/custom vaults resolve correctly.Show-Guerrillapasses the active vault name automatically.Get-SafehouseSecretprivate helper — a graceful counterpart toGet-GuerrillaCredentialthat returns$nullon a miss (vault/key absent, SecretManagement not installed) instead of throwing, for "fall back to the safehouse" resolution.
- Active Directory was already covered:
Invoke-Reconnaissancefalls back to the current Kerberos session, so it needs no vault credentials.
- Show-Guerrilla scans failed for Google Workspace / Entra / Campaign with
A parameter cannot be found that matches parameter name 'ScanMode'. The GUI built its scan arguments from hardcoded per-cmdlet name lists that didn't match the cmdlets' real parameters: none ofInvoke-Fortification/Invoke-Infiltration/Invoke-Reconnaissancedeclare-ScanMode, andInvoke-Campaignhas neither-Categoriesnor-NoReports. The action now inspects the target cmdlet's actual parameter set via(Get-Command $Cmdlet).Parametersand only passes options the cmdlet declares, so every theater binds cleanly. (AD scans already worked becauseInvoke-Reconnaissancehappened not to be on the-ScanModelist.)
-
Show-Guerrilla scans failed instantly with
The term 'Invoke-Reconnaissance' is not recognized. Two bugs in the worker runspace that drives a scan:- The module was never imported into the runspace — the code used
InitialSessionState.ImportPSModule()with a full.psd1path, but that API expects a module name and silently does nothing with a path, so the runspace started with none of Guerrilla's commands. The worker now callsImport-Module <manifest> -ErrorAction Stopexplicitly (with-Verbose:$falseso the import's own load messages don't flood the scan log). - The scan action was passed across the runspace boundary as a live scriptblock object, which retains affinity to the GUI runspace that created it — so it ran against the wrong runspace/thread and couldn't see the module even once imported (and could corrupt the engine). The action is now marshalled as source text and rehydrated inside the worker via
[scriptblock]::Create().
No scan could be launched from the GUI before this fix. The CLI cmdlets were unaffected.
- The module was never imported into the runspace — the code used
-
Tests/Manual/Test-GuiAsyncDrain.ps1now resolves a real exported cmdlet (Invoke-Reconnaissance) inside the worker runspace, so this class of "module not loaded in the worker" regression is caught by the harness rather than only in the live GUI.
- GUI scan log looked hung / showed nothing useful during a scan. The scan runspace's
Write-ProgressLineoutput is emitted as severalWrite-Host -NoNewlinefragments carrying ANSI colour codes. The GUI now strips the ANSI escapes and reassembles the fragments into clean whole lines (e.g.[1750 UTC] RECON > Connecting to Active Directory) instead of dropping or garbling them, so live per-phase progress actually appears. - Footer version was hardcoded to
v2.3.0; it now readsModuleVersionfrom the manifest so it can't drift.
- GUI scan log now also surfaces the Warning stream and shows a
... still working (Ns elapsed)heartbeat when a phase goes quiet for more than 5s, so long AD collection phases no longer look hung. Tests/Manual/Test-GuiAsyncDrain.ps1— a headless WPF-dispatcher harness that exercises the async drain (fragment reassembly, ANSI stripping, warnings, heartbeat, completion).
- GUI: "Run Scan" crashed when a scan finished. The
OnLog/OnComplete/OnErrorcallbacks inShow-GuerrillaWindowwere built viaGetNewClosure()inside the Run-button click handler.GetNewClosure()snapshots only that handler's own locals — not the function-scope helpers it references ($appendLog,$resetOperationsUI,$session,$brushes), which are merely visible through the scope chain — so they resolved to$nullwhen the DispatcherTimer fired the callbacks (The expression after '&' in a pipeline element produced an object that was not valid). The helpers are now localized into the handler scope before the closures are built. This bug was latent since the GUI shipped in 2.3.0 and only surfaced once 2.4.0 fixed the async path to actually fire completion callbacks. - GUI: a throwing callback could wedge the window.
Invoke-GuerrillaGuiAsyncnow guards everyOnComplete/OnErrorinvocation and downgrades a failure to aWrite-Warninginstead of letting it escape the timer tick as a raw console error.
- GUI: scan results never reached the window. The
Invoke-GuerrillaGuiAsyncDispatcherTimer tick handler was not a closure, so$state/$OnComplete/$OnErrorwere unresolvable when the timer fired; additionally, results were read fromEndInvoke(always empty with the explicit-outputBeginInvokeoverload introduced in 2.3.1) instead of the output collection. Scans now complete, stream logs, and report results; a stray non-terminating error no longer discards a successful scan. - Vault-staged Google service-account key leaked to %TEMP%.
Invoke-Recon,Invoke-Fortification, andInvoke-Campaignstaged the private key to a temp file and never deleted it. Scan bodies are now wrapped intry/finallycleanup. - Set-Safehouse could destroy the only copy of a key. The "delete the original key file?" prompt ran before the vault write; deletion is now offered only after
Set-GuerrillaCredentialsucceeds. Save-TheaterStatenow writes atomically (temp file + rename), matchingSave-OperationState— a crash mid-write can no longer corrupt theater state.Register-Patrolescapes single quotes when embedding paths in the generated patrol-runner script (apostrophes in profile paths no longer break it).Send-SignalSyslogflattens CR/LF/tab and escapes CEF/LEEF metacharacters in threat-derived fields, preventing log-line forgery via crafted indicator text.Get-Safehouseagain masks plaintext secrets found in config.json (lost in the 2.2.0 vault redesign), warns when no config exists, and no longer hard-fails when SecretManagement is not installed.Show-Guerrillavalidates the thread is STA before loading WPF, replacing an opaque failure underpwsh -MTA.- Renamed shadowed automatic variables:
$errorinInvoke-IntuneChecks,$matchesin the threat-actor matcher (nowFind-ThreatActorProfile, approved verb).
- All
Invoke-RestMethodcalls now carry explicit timeouts: 30s for alert senders, token endpoints, geo/intel lookups; 120s for Graph/Azure RM/Google Admin API wrappers. A hung endpoint can no longer stall a patrol indefinitely. - Removed ~30 dead variable assignments across check runners, exporters, and senders (PSScriptAnalyzer
PSUseDeclaredVarsMoreThanAssignmentsis now clean for Public/Private). Set-Safehouse/Get-Safehouse/config-migration tests rewritten against the 2.2.0+ vault API (they still targeted the retired-AdminEmail/-SendGridApiKeysurface).
- Renamed module from PSRecon to Guerrilla with guerrilla warfare-themed cmdlet names
Invoke-GoogleRecon->Invoke-ReconGet-ReconAlerts->Get-DeadDropSend-ReconAlert->Send-SignalSend-ReconAlertSendGrid->Send-SignalSendGridSend-ReconAlertMailgun->Send-SignalMailgunSend-ReconAlertTwilio->Send-SignalTwilioSet-ReconConfig->Set-SafehouseGet-ReconConfig->Get-SafehouseRegister-ReconScheduledTask->Register-PatrolUnregister-ReconScheduledTask->Unregister-PatrolGet-ReconScheduledTask->Get-Patrol- Reorganized Private functions into subdirectories:
Core/,Google/,Export/,Console/ - Updated all type names:
PSRecon.*->Guerrilla.* - Updated config/state paths:
$APPDATA/PSRecon->$APPDATA/Guerrilla - Updated scheduled task name:
PSRecon-ScheduledScan->Guerrilla-Patrol - Updated all branding strings and alert content
- Backward-compatible aliases for all 11 old PSRecon function names
- Automatic config migration from
$APPDATA/PSReconto$APPDATA/Guerrilla - MIT License
- This changelog
- Initial release as PSRecon
- Google Workspace compromise assessment via Admin Reports API
- 7 threat detection signals (known attacker IPs, cloud IP logins, reauth from cloud, risky actions, suspicious country logins, OAuth from cloud, cloud-only logins)
- Threat scoring engine with CRITICAL/HIGH/MEDIUM/LOW/Clean levels
- Incremental scanning with watermark-based state tracking
- HTML, CSV, and JSON report generation
- Alert dispatching via SendGrid, Mailgun, and Twilio SMS
- Scheduled task registration for automated scanning
- GeoIP enrichment via ip-api.com batch API
- CIDR-based cloud provider IP classification (AWS + general cloud/hosting)
- Known attacker IP database with exact-match detection
- Suspicious country login detection (19 countries)
- Config management with JSON persistence