Skip to content

Commit 2676155

Browse files
author
r0BIT
committed
refactor: Clean up credential validation output
- Remove legacy [+], [-], [?] prefixes (Rich handles coloring now) - Update color detection to use VALID/INVALID/BLOCKED/UNKNOWN keywords - Messages are now cleaner: 'VALID', 'INVALID (wrong password)', etc.
1 parent b0618b7 commit 2676155

File tree

4 files changed

+38
-29
lines changed

4 files changed

+38
-29
lines changed

taskhound/classification.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def _analyze_password_age(
9393
if hv and hv.loaded:
9494
risk_level, pwd_analysis = hv.analyze_password_age(runas, task_date)
9595
if risk_level != "UNKNOWN":
96-
return pwd_analysis
96+
return f"{risk_level}: {pwd_analysis}"
9797

9898
# Fall back to pre-fetched LDAP data if BloodHound not available
9999
if pwd_cache and task_date:
@@ -108,7 +108,7 @@ def _analyze_password_age(
108108
if pwd_last_set:
109109
risk_level, pwd_analysis = _analyze_password_freshness(task_date, pwd_last_set)
110110
if risk_level != "UNKNOWN":
111-
return pwd_analysis
111+
return f"{risk_level}: {pwd_analysis}"
112112
except Exception as e:
113113
from .utils.logging import debug
114114
debug(f"Password analysis failed for {runas}: {e}")

taskhound/output/printer.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,22 @@ def print_task_table(
6464
if label == "Decrypted Pwd" and value:
6565
value_style = COLORS["password"]
6666
elif label == "Cred Validation":
67-
if "[+]" in value:
67+
if "VALID" in value.upper() and "INVALID" not in value.upper():
6868
value_style = COLORS["success"]
69-
elif "[-]" in value:
69+
elif "INVALID" in value.upper() or "BLOCKED" in value.upper():
7070
value_style = COLORS["error"]
71-
elif "[?]" in value:
71+
elif "UNKNOWN" in value.upper():
7272
value_style = COLORS["warning"]
7373
elif label == "Pwd Analysis":
7474
if "GOOD" in value.upper() or "newer" in value.lower():
7575
value_style = COLORS["success"]
7676
elif "BAD" in value.upper() or "stale" in value.lower():
7777
value_style = COLORS["warning"]
78+
# Strip the GOOD:/BAD: prefix from display (used only for color detection)
79+
if value.upper().startswith("GOOD: "):
80+
value = value[6:]
81+
elif value.upper().startswith("BAD: "):
82+
value = value[5:]
7883
elif label == "Enabled":
7984
if value.lower() == "true":
8085
value_style = COLORS["success"]
@@ -353,19 +358,19 @@ def format_block(
353358
# Build status display
354359
if cred_status == "unknown":
355360
if password_analysis and "GOOD" in password_analysis.upper():
356-
status_display = "[+] LIKELY VALID (task never ran, but password newer than pwdLastSet)"
361+
status_display = "LIKELY VALID (task never ran, password newer than pwdLastSet)"
357362
elif password_analysis and "BAD" in password_analysis.upper():
358-
status_display = "[-] LIKELY INVALID (task never ran, password older than pwdLastSet)"
363+
status_display = "LIKELY INVALID (task never ran, password older than pwdLastSet)"
359364
else:
360-
status_display = f"[?] UNKNOWN - task never ran ({cred_code})"
365+
status_display = f"UNKNOWN - task never ran ({cred_code})"
361366
elif cred_valid is True:
362-
status_display = "[+] VALID (hijackable)" if cred_hijackable else f"[+] VALID (restricted: {cred_status})"
367+
status_display = "VALID" if cred_hijackable else f"VALID (restricted: {cred_status})"
363368
elif cred_status == "invalid":
364-
status_display = "[-] INVALID (wrong password)"
369+
status_display = "INVALID (wrong password)"
365370
elif cred_status == "blocked":
366-
status_display = "[-] BLOCKED (account disabled/expired)"
371+
status_display = "BLOCKED (account disabled/expired)"
367372
else:
368-
status_display = f"[?] {cred_status} ({cred_code})"
373+
status_display = f"{cred_status} ({cred_code})"
369374

370375
rows.append(("Cred Validation", status_display))
371376

tests/test_classification.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_returns_analysis_when_risk_known(self):
7676
meta = {"date": "2024-01-01T00:00:00"}
7777
result = _analyze_password_age(hv, "[email protected]", meta, "\\Task")
7878

79-
assert result == "Password 180+ days old"
79+
assert result == "HIGH: Password 180+ days old"
8080
hv.analyze_password_age.assert_called_once()
8181

8282
def test_returns_none_when_risk_unknown(self):
@@ -104,7 +104,7 @@ def test_warns_when_using_fallback_date(self):
104104
assert "no explicit creation date" in mock_warn.call_args[0][0]
105105
assert "TestTask" in mock_warn.call_args[0][0]
106106
# Should still return the analysis result even with fallback warning
107-
assert result == "Analysis result"
107+
assert result == "HIGH: Analysis result"
108108

109109

110110
class TestClassifyTask:
@@ -137,7 +137,7 @@ def test_tier0_with_stored_credentials(self):
137137

138138
assert result.task_type == "TIER-0"
139139
assert result.reason == "Domain Admin member"
140-
assert result.password_analysis == "Password old"
140+
assert result.password_analysis == "HIGH: Password old"
141141
assert result.should_include is True
142142
assert row.type == TaskType.TIER0.value
143143

@@ -426,7 +426,7 @@ def test_pwd_cache_fallback_when_no_bloodhound(self):
426426
pwd_cache=pwd_cache,
427427
)
428428

429-
assert result == "Password changed after task creation"
429+
assert result == "GOOD: Password changed after task creation"
430430
mock_analyze.assert_called_once()
431431

432432
def test_pwd_cache_normalizes_domain_username(self):
@@ -449,7 +449,7 @@ def test_pwd_cache_normalizes_domain_username(self):
449449
pwd_cache=pwd_cache,
450450
)
451451

452-
assert result == "Password older than task"
452+
assert result == "BAD: Password older than task"
453453

454454
def test_pwd_cache_handles_simple_username(self):
455455
"""Should handle username without domain prefix."""
@@ -471,7 +471,7 @@ def test_pwd_cache_handles_simple_username(self):
471471
pwd_cache=pwd_cache,
472472
)
473473

474-
assert result == "Password fresh"
474+
assert result == "GOOD: Password fresh"
475475

476476
def test_pwd_cache_returns_none_when_user_not_found(self):
477477
"""Should return None when user not in pwd_cache."""
@@ -569,7 +569,7 @@ def test_pwd_cache_skipped_when_bloodhound_has_data(self):
569569
)
570570

571571
# Should use BloodHound result, not pwd_cache
572-
assert result == "BloodHound analysis"
572+
assert result == "HIGH: BloodHound analysis"
573573

574574
def test_pwd_cache_used_when_bloodhound_returns_unknown(self):
575575
"""Should fall back to pwd_cache when BloodHound returns UNKNOWN."""
@@ -595,7 +595,7 @@ def test_pwd_cache_used_when_bloodhound_returns_unknown(self):
595595
pwd_cache=pwd_cache,
596596
)
597597

598-
assert result == "Cache-based analysis"
598+
assert result == "GOOD: Cache-based analysis"
599599

600600

601601
class TestClassifyTaskWithPwdCache:

tests/test_printer.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,9 @@ def test_cred_validation_valid_hijackable(self, mock_resolve):
382382
)
383383

384384
text = "\n".join(lines)
385-
assert "[+] VALID (hijackable)" in text
385+
assert "Cred Validation" in text
386+
assert "VALID" in text
387+
assert "INVALID" not in text
386388

387389
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
388390
def test_cred_validation_valid_restricted(self, mock_resolve):
@@ -405,7 +407,7 @@ def test_cred_validation_valid_restricted(self, mock_resolve):
405407
)
406408

407409
text = "\n".join(lines)
408-
assert "[+] VALID (restricted: logon_as_batch)" in text
410+
assert "VALID (restricted: logon_as_batch)" in text
409411
assert "Cred Detail : User has SeLogonAsBatchJob right" in text
410412

411413
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
@@ -427,7 +429,7 @@ def test_cred_validation_invalid(self, mock_resolve):
427429
)
428430

429431
text = "\n".join(lines)
430-
assert "[-] INVALID (wrong password)" in text
432+
assert "INVALID (wrong password)" in text
431433

432434
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
433435
def test_cred_validation_blocked(self, mock_resolve):
@@ -447,7 +449,7 @@ def test_cred_validation_blocked(self, mock_resolve):
447449
)
448450

449451
text = "\n".join(lines)
450-
assert "[-] BLOCKED (account disabled/expired)" in text
452+
assert "BLOCKED (account disabled/expired)" in text
451453

452454
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
453455
def test_cred_validation_unknown_with_good_password(self, mock_resolve):
@@ -469,7 +471,7 @@ def test_cred_validation_unknown_with_good_password(self, mock_resolve):
469471
)
470472

471473
text = "\n".join(lines)
472-
assert "[+] LIKELY VALID (task never ran, but password newer than pwdLastSet)" in text
474+
assert "LIKELY VALID (task never ran, password newer than pwdLastSet)" in text
473475

474476
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
475477
def test_cred_validation_unknown_with_bad_password(self, mock_resolve):
@@ -491,7 +493,7 @@ def test_cred_validation_unknown_with_bad_password(self, mock_resolve):
491493
)
492494

493495
text = "\n".join(lines)
494-
assert "[-] LIKELY INVALID (task never ran, password older than pwdLastSet)" in text
496+
assert "LIKELY INVALID (task never ran, password older than pwdLastSet)" in text
495497

496498
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
497499
def test_cred_validation_unknown_no_analysis(self, mock_resolve):
@@ -512,7 +514,7 @@ def test_cred_validation_unknown_no_analysis(self, mock_resolve):
512514
)
513515

514516
text = "\n".join(lines)
515-
assert "[?] UNKNOWN - task never ran (0x80070005)" in text
517+
assert "UNKNOWN - task never ran (0x80070005)" in text
516518

517519
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
518520
def test_cred_validation_other_status(self, mock_resolve):
@@ -533,7 +535,7 @@ def test_cred_validation_other_status(self, mock_resolve):
533535
)
534536

535537
text = "\n".join(lines)
536-
assert "[?] custom_status (0x12345678)" in text
538+
assert "custom_status (0x12345678)" in text
537539

538540

539541
class TestFormatBlockTaskKind:
@@ -577,7 +579,9 @@ def test_task_with_cred_validation(self, mock_resolve):
577579
)
578580

579581
text = "\n".join(lines)
580-
assert "[+] VALID (hijackable)" in text
582+
assert "Cred Validation" in text
583+
assert "VALID" in text
584+
assert "INVALID" not in text
581585

582586
@patch("taskhound.output.printer.format_runas_with_sid_resolution")
583587
def test_task_with_decrypted_credentials(self, mock_resolve):

0 commit comments

Comments
 (0)