diff --git a/cli/internal/scoreboard/transport_ares.go b/cli/internal/scoreboard/transport_ares.go index 77215f03..7219f265 100644 --- a/cli/internal/scoreboard/transport_ares.go +++ b/cli/internal/scoreboard/transport_ares.go @@ -200,6 +200,7 @@ var creditableCategories = map[string]string{ "laps_password_read": "laps_password_read", "rbcd": "rbcd", "nopac": "nopac", + "printnightmare": "printnightmare", } // uncreditableCategories are ares categories that must never produce technique @@ -210,16 +211,22 @@ var creditableCategories = map[string]string{ // - "golden_ticket" is flat in ares but per-domain in the answer key // (golden_ticket-). That credit comes from domain_compromise[], // which carries the domain the flat category drops. -// - "printnightmare" and "zerologon" are uncreditable by design: ares mints -// both on evidence that precedes success (printnightmare accepts "Stub -// loaded"/"[+] Triggering"; zerologon only runs the nxc check module, -// never the reset). ares-cli #366 promoted them out of "other", so without -// an explicit refusal they would start crediting attempts as exploits. +// - "zerologon" is uncreditable by design: ares only ever runs the nxc +// zerologon check module, never the password reset, so the category counts +// detections and crediting it would score a scan as an exploit. ares-cli +// #366 promoted it out of "other", so without an explicit refusal it would +// start crediting scans. +// +// "printnightmare" was refused here on the same grounds until ares-cli #367. +// The rationale was wrong about the mechanism: its gate matched five markers +// cube0x0/CVE-2021-1675 never prints, so it could not fire at all and the +// technique scored zero rather than over-crediting. #367 rebuilt the gate on a +// single "exploit completed" match taken from the PoC's real output, so the +// category now means what it says and is credited like any other. var uncreditableCategories = map[string]bool{ - "other": true, - "golden_ticket": true, - "printnightmare": true, - "zerologon": true, + "other": true, + "golden_ticket": true, + "zerologon": true, } // aresCategoryToTechniqueID returns the answer-key technique ID for an ares diff --git a/cli/internal/scoreboard/transport_ares_test.go b/cli/internal/scoreboard/transport_ares_test.go index a379fcbf..c928192c 100644 --- a/cli/internal/scoreboard/transport_ares_test.go +++ b/cli/internal/scoreboard/transport_ares_test.go @@ -64,13 +64,13 @@ func TestAresCategoryToTechniqueID(t *testing.T) { {"esc8 credits", "adcs_esc8", "adcs_esc8"}, {"long esc form is distinct", "adcs_esc10_case1", "adcs_esc10_case1"}, {"nopac credits after ares #366", "nopac", "nopac"}, + {"printnightmare credits after ares #367", "printnightmare", "printnightmare"}, // The one name that differs between the two vocabularies. {"forest_trust aliases to cross_forest_trust", "forest_trust", "cross_forest_trust"}, - // Refusals: ares mints these on evidence that precedes success, so - // crediting them would score an attempt as an exploit. - {"printnightmare refused", "printnightmare", ""}, + // Refusal: ares runs only the nxc check module, never the password + // reset, so crediting it would score a scan as an exploit. {"zerologon refused", "zerologon", ""}, // Flat in ares, per-domain in the answer key; credited from @@ -139,10 +139,9 @@ func TestEveryAresCategoryIsClassified(t *testing.T) { // rather than added to silence a failing run. func TestUncreditableCategoriesAreDeliberate(t *testing.T) { want := map[string]bool{ - "other": true, - "golden_ticket": true, - "printnightmare": true, - "zerologon": true, + "other": true, + "golden_ticket": true, + "zerologon": true, } if !reflect.DeepEqual(uncreditableCategories, want) { t.Errorf("uncreditableCategories = %v, want %v; a new refusal needs its rationale in the doc comment", @@ -196,7 +195,7 @@ func TestDetectTokenCoverageDrift(t *testing.T) { }, { name: "deliberate refusals are exempt", - coverage: cov("other", 5, "golden_ticket", 2, "printnightmare", 1, "zerologon", 1), + coverage: cov("other", 5, "golden_ticket", 2, "zerologon", 1), want: nil, }, { @@ -267,13 +266,21 @@ func TestWriteTokenCoverageEntries(t *testing.T) { }, { name: "refusals never credit", + coverage: map[string]aresTokenBucket{ + "zerologon": {Exploited: 1}, + "golden_ticket": {Exploited: 2}, + "other": {Exploited: 9}, + }, + absent: []string{"tech:zerologon", "tech:golden_ticket", "tech:other"}, + }, + { + // Refused alongside zerologon until ares-cli #367 rebuilt its gate + // on a marker the PoC actually prints. + name: "printnightmare credits after ares gate fix", coverage: map[string]aresTokenBucket{ "printnightmare": {Exploited: 3}, - "zerologon": {Exploited: 1}, - "golden_ticket": {Exploited: 2}, - "other": {Exploited: 9}, }, - absent: []string{"tech:printnightmare", "tech:zerologon", "tech:golden_ticket", "tech:other"}, + want: []string{"tech:printnightmare"}, }, { name: "alias is credited under the answer-key id",