mirror of
https://github.com/peass-ng/PEASS-ng.git
synced 2026-07-30 23:50:27 -07:00
* feat: MITRE ATT&CK integration for LinPEAS and WinPEAS - Add -T T1234,T5678 flag to LinPEAS to filter checks by technique - Add mitre=T1234,T5678 argument to WinPEAS for technique-based filtering - Annotate every check title with MITRE technique ID(s) displayed in grey - Add $_mitre_tag to Generated Global Variables in 0_variables_base.sh - Add check_mitre_filter() shell function with prefix-match support - Add MitreAttackIds property to ISystemCheck interface (C#) - Update MainPrint/GreatPrint in Beaprint.cs to accept optional mitreIds - Tag all 158 LinPEAS check modules with # Mitre: metadata - Tag all 16 WinPEAS check classes with MitreAttackIds property - Update linpeasModule.py to parse # Mitre: metadata field - Update linpeasBaseBuilder.py to emit check_mitre_filter wrappers - Add 3 MITRE argument parsing tests to ArgumentParsingTests.cs * test: add MITRE filter coverage for LinPEAS builder and WinPEAS LinPEAS (test_builder.py): - test_mitre_flag_present_in_getopts: -T: must appear in getopts string - test_mitre_flag_present_in_help_text: -T must appear in built help text - test_mitre_filter_function_present: check_mitre_filter() must be in built script WinPEAS (ArgumentParsingTests.cs): - PassesMitreFilter_EmptyFilter_AllChecksPass: no filter -> all checks run - PassesMitreFilter_ExactMatch_Passes: T1082 filter matches T1082 check - PassesMitreFilter_NoMatch_Fails: T1082 filter rejects T1057 check - PassesMitreFilter_PrefixMatch_Passes: T1552 filter matches T1552.001/T1552.005 - PassesMitreFilter_SubtechniqueDoesNotMatchDifferentBase_Fails: T1548 != T1552.001 * chore: ignore .github/instructions/ and untrack todos.instructions.md * fix: complete and accurate MITRE ATT&CK mappings for LinPEAS and WinPEAS gitignore: - Add .github/instructions/ to .gitignore and untrack todos.instructions.md LinPEAS — corrected mappings: - 29_Interesting_environment_variables.sh: add missing T1552.007,T1082 - 3_USBCreator.sh: T1548 → T1548.003,T1068 (polkit bypass + CVE-class exploit) - 9_Doas.sh: T1548 → T1548.003 (doas is a sudo/sudo-caching equivalent) - 10_Pkexec.sh: T1548 → T1548.003,T1548.004,T1068 per-section specificity - 2_Process_cred_in_memory.sh: T1003,T1055 → T1003.007 (Proc Filesystem, drop wrong T1055) - 11_Superusers.sh: T1087.001,T1548 → T1087.001 (discovery only, no elevation abuse) - 14/15/16 writable files: T1574 → T1574.009,T1574.010 (specific sub-techniques) WinPEAS — corrected mappings: - SystemInfo: class expanded to full technique union; WSUS T1195→T1072,T1068; KrbRelayUp T1558→T1187,T1558; Object Manager T1548→T1068; Named Pipes T1559.001→T1559; Low-priv pipes T1559.001→T1134.001,T1559 - EventsInfo: class expanded with T1078.003,T1552.001,T1059.001,T1082 - UserInfo: class expanded; Token privileges T1134→T1134.001 - ProcessInfo: Leaked Handlers T1134.003→T1134.001 (token impersonation, not make-token) - ServicesInfo: class adds T1574.011,T1068 - ApplicationsInfo: class adds T1010,T1014 - NetworkInfo: class adds T1018,T1090 - ActiveDirectoryInfo: T1484→T1484.001; class adds T1003 - WindowsCreds: class sub-techniques T1552→T1552.001,T1552.002, T1555→T1555.003,T1555.004; SSClient T1059→T1552.001 (wrong technique entirely) - FilesInfo: class expanded with T1552.002,T1552.004,T1552.006,T1564.001,T1574.001, T1059.004,T1114.001,T1218,T1649; Cloud Credentials T1552.005→T1552.001 - SoapClientInfo: T1059,T1071→T1559,T1071.001 (IPC/Web protocol, not scripting) * fix: add missing T1613 and T1562.001 to SystemInfo class-level MitreAttackIds; label AD object enumeration with T1087.002 and T1018 * fix: correct linpeas mitre filter matching logic * fix: MITRE code bugs — pass-through for untagged checks, remove dead OR in section gate - PassesMitreFilter (Checks.cs): when MitreAttackIds is null or empty and a filter is active, return true (pass-through) instead of false. Previously any future ISystemCheck added without MITRE IDs would be silently excluded by an active filter. - linpeasBaseBuilder.py: remove redundant '|| [ -z "$MITRE_FILTER" ]' from the generated section-level gate. check_mitre_filter already returns 0 immediately when MITRE_FILTER is empty, so the OR branch was unreachable and inconsistent with the check-level gate which uses the same function without the extra guard. - ArgumentParsingTests.cs: add PassesMitreFilter_NullMitreAttackIds_PassesThrough and PassesMitreFilter_EmptyMitreAttackIds_PassesThrough regression tests. * fix(mitre): 4 bugs — dead arg parser, wait logic, subprocess forks, cleanup race Checks.cs: max-regex-file-size used string.Equals which requires exact match, so 'max-regex-file-size=500000' could never match and MaxRegexFileSize was stuck at 1000000 forever. Fixed to arg.StartsWith. Checks.cs RunChecks: wait compared loop index i against _systemCheckSelectedKeysHashSet.Count, which is 0 when all checks run (so i < -1 is always false) and semantically wrong when a key subset is selected. Replaced with a pre-count of checks that pass both filters and a running counter. 0_variables_base.sh check_mitre_filter: replaced two $(echo ... | tr ...) subprocess forks per call with pure parameter-expansion while-loops. Zero process forks, POSIX-compliant, ~632 fork()s saved per full filtered run. Declares _mitre_tags_left and _mitre_filters_left in Generated Global Variables. linpeas_builder.py: os.remove of the shared temp file raised FileNotFoundError when multiple sequential builder invocations ran (the second saw the file already deleted by the first). Wrapped in try/except FileNotFoundError. Tests: Added PassesMitreFilter_SubtechniqueFilter_DoesNotMatchParentOnlyTag and MaxRegexFileSize_ArgParsed_Correctly regression tests (16 total). * ci: add manual build-artifacts workflow (winPEAS.exe + linpeas.sh) * fix(linpeas): getopts silent mode — clear error when -T given without argument Switch getopts to silent mode (leading ':') so the shell does not emit its own terse 'No arg for -T option' message. Add explicit :) case that prints ERROR: -T requires an argument (e.g. -T T1082,T1552) and then dumps the help text before exiting 1. Add *) case for unrecognised flags with the same pattern. Behaviour for all valid flags is unchanged. * chore: untrack build-artifacts workflow, add to .gitignore
245 lines
11 KiB
C#
245 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace winPEAS.Tests
|
|
{
|
|
[TestClass]
|
|
public class ArgumentParsingTests
|
|
{
|
|
private static bool InvokeIsNetworkTypeValid(string arg)
|
|
{
|
|
var method = typeof(winPEAS.Checks.Checks).GetMethod("IsNetworkTypeValid", BindingFlags.NonPublic | BindingFlags.Static);
|
|
Assert.IsNotNull(method, "IsNetworkTypeValid method not found.");
|
|
return (bool)method.Invoke(null, new object[] { arg });
|
|
}
|
|
|
|
private static bool InvokePassesMitreFilter(string[] checkIds)
|
|
{
|
|
// Build a minimal ISystemCheck stub whose MitreAttackIds returns checkIds.
|
|
var stub = new MitreCheckStub(checkIds);
|
|
var method = typeof(winPEAS.Checks.Checks).GetMethod("PassesMitreFilter", BindingFlags.NonPublic | BindingFlags.Static);
|
|
Assert.IsNotNull(method, "PassesMitreFilter method not found.");
|
|
return (bool)method.Invoke(null, new object[] { stub });
|
|
}
|
|
|
|
/// <summary>Minimal ISystemCheck stub for PassesMitreFilter reflection tests.</summary>
|
|
private sealed class MitreCheckStub : winPEAS.Checks.ISystemCheck
|
|
{
|
|
public MitreCheckStub(string[] ids) { MitreAttackIds = ids; }
|
|
public string[] MitreAttackIds { get; }
|
|
public void PrintInfo(bool isDebug) { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all public static Checks fields that arg parsing can mutate, then
|
|
/// invokes Program.Main with the supplied args followed by "--help" so execution
|
|
/// returns immediately after parsing without running any actual system checks.
|
|
/// </summary>
|
|
private static void ParseOnly(params string[] args)
|
|
{
|
|
// Reset every field that Checks.Run() can modify during arg parsing.
|
|
winPEAS.Checks.Checks.IsDomainEnumeration = false;
|
|
winPEAS.Checks.Checks.IsNoColor = false;
|
|
winPEAS.Checks.Checks.DontCheckHostname = false;
|
|
winPEAS.Checks.Checks.Banner = true;
|
|
winPEAS.Checks.Checks.IsDebug = false;
|
|
winPEAS.Checks.Checks.IsLinpeas = false;
|
|
winPEAS.Checks.Checks.IsLolbas = false;
|
|
winPEAS.Checks.Checks.IsNetworkScan = false;
|
|
winPEAS.Checks.Checks.SearchProgramFiles = false;
|
|
winPEAS.Checks.Checks.NetworkScanOptions = string.Empty;
|
|
winPEAS.Checks.Checks.PortScannerPorts = null;
|
|
winPEAS.Checks.Checks.LinpeasUrl = "https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh";
|
|
winPEAS.Checks.Checks.MaxRegexFileSize = 1000000;
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
|
|
var argsWithHelp = args.Concat(new[] { "--help" }).ToArray();
|
|
Program.Main(argsWithHelp);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ShouldAcceptValidNetworkTypes()
|
|
{
|
|
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=auto"));
|
|
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10"));
|
|
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10/24"));
|
|
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10,10.10.10.20"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ShouldRejectInvalidNetworkTypes()
|
|
{
|
|
Assert.IsFalse(InvokeIsNetworkTypeValid("-network="));
|
|
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.999"));
|
|
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.10/64"));
|
|
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=999.999.999.999/24"));
|
|
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=not-an-ip"));
|
|
}
|
|
|
|
// -- Space-separated argument normalisation tests --
|
|
|
|
[TestMethod]
|
|
public void NetworkFlag_SpaceSeparated_Netmask_SetsIsNetworkScan()
|
|
{
|
|
ParseOnly("-network", "10.0.0.0/24");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.IsNetworkScan,
|
|
"-network 10.0.0.0/24 (space-separated) should set IsNetworkScan");
|
|
Assert.AreEqual("10.0.0.0/24", winPEAS.Checks.Checks.NetworkScanOptions);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NetworkFlag_SpaceSeparated_Auto_SetsIsNetworkScan()
|
|
{
|
|
ParseOnly("-network", "auto");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.IsNetworkScan,
|
|
"-network auto (space-separated) should set IsNetworkScan");
|
|
Assert.IsTrue(string.Equals("auto", winPEAS.Checks.Checks.NetworkScanOptions, StringComparison.OrdinalIgnoreCase),
|
|
"-network auto (space-separated) should set IsNetworkScan");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NetworkFlag_EqualsSeparated_Netmask_SetsIsNetworkScan()
|
|
{
|
|
ParseOnly("-network=10.0.0.0/24");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.IsNetworkScan,
|
|
"-network=10.0.0.0/24 (equals-separated) should set IsNetworkScan");
|
|
Assert.AreEqual("10.0.0.0/24", winPEAS.Checks.Checks.NetworkScanOptions);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void NetworkAndPortsFlags_SpaceSeparated_BothParsedCorrectly()
|
|
{
|
|
ParseOnly("-network", "auto", "-ports", "80,443");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.IsNetworkScan,
|
|
"-network auto -ports 80,443 should set IsNetworkScan");
|
|
var ports = winPEAS.Checks.Checks.PortScannerPorts?.ToList();
|
|
Assert.IsNotNull(ports, "PortScannerPorts should not be null");
|
|
CollectionAssert.AreEquivalent(new List<int> { 80, 443 }, ports);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MitreFlag_SingleTechnique_ParsedIntoFilter()
|
|
{
|
|
ParseOnly("mitre=T1082");
|
|
Assert.AreEqual(1, winPEAS.Checks.Checks.MitreFilter.Count,
|
|
"mitre=T1082 should add exactly one technique to MitreFilter");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.MitreFilter.Contains("T1082"),
|
|
"MitreFilter should contain T1082");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MitreFlag_MultipleIds_AllParsedIntoFilter()
|
|
{
|
|
ParseOnly("mitre=T1082,T1548.002,T1057");
|
|
Assert.AreEqual(3, winPEAS.Checks.Checks.MitreFilter.Count,
|
|
"mitre=T1082,T1548.002,T1057 should add three techniques to MitreFilter");
|
|
Assert.IsTrue(winPEAS.Checks.Checks.MitreFilter.Contains("T1082"));
|
|
Assert.IsTrue(winPEAS.Checks.Checks.MitreFilter.Contains("T1548.002"));
|
|
Assert.IsTrue(winPEAS.Checks.Checks.MitreFilter.Contains("T1057"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MitreFlag_CaseInsensitive_IsRecognised()
|
|
{
|
|
ParseOnly("MITRE=t1082");
|
|
Assert.AreEqual(1, winPEAS.Checks.Checks.MitreFilter.Count,
|
|
"MITRE= (upper-case) should be accepted case-insensitively");
|
|
// HashSet uses OrdinalIgnoreCase so both casing variants should be found
|
|
Assert.IsTrue(winPEAS.Checks.Checks.MitreFilter.Contains("T1082") ||
|
|
winPEAS.Checks.Checks.MitreFilter.Contains("t1082"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_EmptyFilter_AllChecksPass()
|
|
{
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
Assert.IsTrue(InvokePassesMitreFilter(new[] { "T1082" }),
|
|
"An empty MitreFilter should pass every check.");
|
|
Assert.IsTrue(InvokePassesMitreFilter(new string[0]),
|
|
"An empty MitreFilter should pass a check with no IDs.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_ExactMatch_Passes()
|
|
{
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1082");
|
|
Assert.IsTrue(InvokePassesMitreFilter(new[] { "T1082" }),
|
|
"A check tagged T1082 should pass when filter contains T1082.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_NoMatch_Fails()
|
|
{
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1082");
|
|
Assert.IsFalse(InvokePassesMitreFilter(new[] { "T1057" }),
|
|
"A check tagged T1057 should not pass when filter only contains T1082.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_PrefixMatch_Passes()
|
|
{
|
|
// Filter on base technique T1552 should match sub-technique T1552.001
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1552");
|
|
Assert.IsTrue(InvokePassesMitreFilter(new[] { "T1552.001" }),
|
|
"Filter on T1552 should match a check tagged T1552.001 (prefix match).");
|
|
Assert.IsTrue(InvokePassesMitreFilter(new[] { "T1552.005" }),
|
|
"Filter on T1552 should match a check tagged T1552.005 (prefix match).");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_SubtechniqueDoesNotMatchDifferentBase_Fails()
|
|
{
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1548");
|
|
Assert.IsFalse(InvokePassesMitreFilter(new[] { "T1552.001" }),
|
|
"Filter on T1548 must not match T1552.001.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_NullMitreAttackIds_PassesThrough()
|
|
{
|
|
// A check with null MitreAttackIds should NOT be silently excluded
|
|
// when a filter is active — it simply has no metadata to match against.
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1082");
|
|
Assert.IsTrue(InvokePassesMitreFilter(null),
|
|
"A check with null MitreAttackIds should pass through (return true) when a filter is active.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_EmptyMitreAttackIds_PassesThrough()
|
|
{
|
|
// A check that declares string[0] should also pass through, not be silently excluded.
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1082");
|
|
Assert.IsTrue(InvokePassesMitreFilter(new string[0]),
|
|
"A check with empty MitreAttackIds should pass through (return true) when a filter is active.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void PassesMitreFilter_SubtechniqueFilter_DoesNotMatchParentOnlyTag()
|
|
{
|
|
// filter=T1552.001 (child) must NOT match a check tagged only with T1552 (parent).
|
|
// Parent filters may broaden to children, but never the reverse.
|
|
winPEAS.Checks.Checks.MitreFilter.Clear();
|
|
winPEAS.Checks.Checks.MitreFilter.Add("T1552.001");
|
|
Assert.IsFalse(InvokePassesMitreFilter(new[] { "T1552" }),
|
|
"A sub-technique filter (T1552.001) must not match a check tagged with only the parent (T1552).");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MaxRegexFileSize_ArgParsed_Correctly()
|
|
{
|
|
ParseOnly("max-regex-file-size=500000");
|
|
Assert.AreEqual(500000, winPEAS.Checks.Checks.MaxRegexFileSize,
|
|
"max-regex-file-size=500000 should set MaxRegexFileSize to 500000.");
|
|
}
|
|
}
|
|
}
|