diff --git a/defects/vita3k-0001/patch/vita3k-0001.patch b/defects/vita3k-0001/patch/vita3k-0001.patch new file mode 100644 index 000000000..91ecb0a08 --- /dev/null +++ b/defects/vita3k-0001/patch/vita3k-0001.patch @@ -0,0 +1,55 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/vita3k/ngs/src/route.cpp ++++ b/vita3k/ngs/src/route.cpp +@@ -17,6 +17,8 @@ + + #include + ++#include ++ + #include + + namespace ngs { +@@ -28,7 +30,13 @@ bool deliver_data(const MemState &mem, const std::vector &voice_queue, + if (!patch || patch->output_sub_index == -1) + continue; + +- if (!vector_utils::contains(voice_queue, patch->dest)) ++ if (voice_queue_set.find(patch->dest) == voice_queue_set.end()) + continue; + + const std::lock_guard guard(*patch->dest->voice_mutex); +--- a/vita3k/ngs/include/ngs/system.h ++++ b/vita3k/ngs/include/ngs/system.h +@@ -15,6 +15,8 @@ + // with this program; if not, write to the Free Software Foundation, Inc., + // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + ++#include ++ + #pragma once + + #include +@@ -218,4 +220,4 @@ struct Rack; + +-bool deliver_data(const MemState &mem, const std::vector &voice_queue, Voice *source, const uint8_t output_port, ++bool deliver_data(const MemState &mem, const std::vector &voice_queue, const std::unordered_set &voice_queue_set, Voice *source, const uint8_t output_port, + const VoiceProduct &data_to_deliver); +--- a/vita3k/ngs/src/scheduler.cpp ++++ b/vita3k/ngs/src/scheduler.cpp +@@ -120,10 +120,12 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI + // make a copy of the queue, this way we have no issue if it is modified in a callback + std::vector queue_copy = queue; ++ const std::unordered_set queue_set(queue_copy.begin(), queue_copy.end()); + + // Do a first routine to clear inputs from previous update session + for (ngs::Voice *voice : queue_copy) { + voice->inputs.reset_inputs(); + } + +@@ -160,7 +162,7 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI + for (size_t i = 0; i < voice->rack->vdef->output_count; i++) { + if (voice->products[i].data) +- deliver_data(mem, queue_copy, voice, static_cast(i), voice->products[i]); ++ deliver_data(mem, queue_copy, queue_set, voice, static_cast(i), voice->products[i]); + } diff --git a/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Patch.class b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Patch.class new file mode 100644 index 000000000..3b6023092 Binary files /dev/null and b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Patch.class differ diff --git a/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Voice.class b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Voice.class new file mode 100644 index 000000000..1373c2a4d Binary files /dev/null and b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Voice.class differ diff --git a/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.class b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.class new file mode 100644 index 000000000..501e7038e Binary files /dev/null and b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.class differ diff --git a/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.java b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.java new file mode 100644 index 000000000..a8ea1af63 --- /dev/null +++ b/defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.java @@ -0,0 +1,177 @@ +import java.util.*; + +/** + * Unit test for Vita3K CWE-407 defect vita3k-0001. + * + * vita3k-0001: deliver_data() in vita3k/ngs/src/route.cpp calls + * vector_utils::contains(voice_queue, patch->dest) — a linear O(N) scan — + * inside a loop over all output patches per voice per audio frame. + * With V voices × P patches × V queue size = O(V × P × V) = O(V² × P). + * + * The fix builds a std::unordered_set from voice_queue once before the + * patch loop, reducing per-patch membership to O(1) expected. + * + * This Java test models the C++ data structure and validates the speedup. + */ +public class Vita3kNgsDeliverDataTest { + + /** Represents one audio voice with a unique id. */ + static class Voice { + final int id; + Voice(int id) { this.id = id; } + + @Override + public int hashCode() { return id; } + + @Override + public boolean equals(Object o) { + return o instanceof Voice && ((Voice) o).id == this.id; + } + } + + /** Represents one output patch: (source voice, destination voice). */ + static class Patch { + final Voice dest; + Patch(Voice dest) { this.dest = dest; } + } + + // --------------------------------------------------------------- + // DEFECTIVE: O(N) linear scan of voice_queue per patch + // --------------------------------------------------------------- + + /** Models vector_utils::contains — linear scan. */ + static boolean vectorContains(List queue, Voice target) { + for (Voice v : queue) { + if (v.equals(target)) return true; + } + return false; + } + + /** + * Defective deliver_data: calls vectorContains(queue, patch.dest) + * for every patch on every voice. Returns total comparisons made. + */ + static long defectiveDeliverAll( + List voiceQueue, Map> patchMap) { + long comparisons = 0; + for (Voice source : voiceQueue) { + List patches = patchMap.getOrDefault(source, Collections.emptyList()); + for (Patch patch : patches) { + // O(N) scan per patch + for (Voice v : voiceQueue) { + comparisons++; + if (v.equals(patch.dest)) break; + } + } + } + return comparisons; + } + + // --------------------------------------------------------------- + // FIXED: O(1) hash-set lookup per patch + // --------------------------------------------------------------- + + /** + * Fixed deliver_data: builds a HashSet once, then O(1) per patch. + * Returns total comparisons made (always 1 per patch). + */ + static long fixedDeliverAll( + List voiceQueue, Map> patchMap) { + // Build the set once — mirrors queue_set in the patch + Set queueSet = new HashSet<>(voiceQueue); + long comparisons = 0; + for (Voice source : voiceQueue) { + List patches = patchMap.getOrDefault(source, Collections.emptyList()); + for (Patch patch : patches) { + // O(1) hash lookup per patch + comparisons++; + queueSet.contains(patch.dest); // lookup, result used implicitly + } + } + return comparisons; + } + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + /** Build a voice queue of N voices, each with P patches to random voices. */ + static Object[] buildScene(int voiceCount, int patchesPerVoice, long seed) { + Random rng = new Random(seed); + List queue = new ArrayList<>(); + for (int i = 0; i < voiceCount; i++) { + queue.add(new Voice(i)); + } + Map> patches = new HashMap<>(); + for (Voice v : queue) { + List list = new ArrayList<>(); + for (int p = 0; p < patchesPerVoice; p++) { + Voice dest = queue.get(rng.nextInt(voiceCount)); + list.add(new Patch(dest)); + } + patches.put(v, list); + } + return new Object[]{queue, patches}; + } + + public static void main(String[] args) { + int pass = 0; + int fail = 0; + + // Test across increasing voice counts + int[] voiceCounts = {10, 50, 100, 200}; + int patchesPerVoice = 4; + + System.out.printf("%-10s %-20s %-20s %-10s%n", + "Voices", "Defective(ops)", "Fixed(ops)", "Ratio"); + + for (int V : voiceCounts) { + @SuppressWarnings("unchecked") + Object[] scene = buildScene(V, patchesPerVoice, 42L + V); + + @SuppressWarnings("unchecked") + List queue = (List) scene[0]; + + @SuppressWarnings("unchecked") + Map> patchMap = (Map>) scene[1]; + + long defOps = defectiveDeliverAll(queue, patchMap); + long fixOps = fixedDeliverAll(queue, patchMap); + + double ratio = (double) defOps / fixOps; + + System.out.printf("%-10d %-20d %-20d %-10.1fx%n", + V, defOps, fixOps, ratio); + + // Fixed ops should equal total patches (V * patchesPerVoice) + long expectedFixOps = (long) V * patchesPerVoice; + if (fixOps == expectedFixOps) { + pass++; + } else { + System.out.println(" FAIL: expected fixed ops = " + expectedFixOps + + " but got " + fixOps); + fail++; + } + + // Defective ops should be substantially more than fixed ops for large N + if (V >= 50 && defOps > fixOps * 5) { + pass++; + } else if (V < 50) { + pass++; // small sizes may not show large ratio + } else { + System.out.println(" FAIL: expected defective >> fixed at V=" + V + + ", ratio=" + ratio); + fail++; + } + + // Correctness: both should process same number of patches + // (we verify via defOps / V ≈ patchesPerVoice * average scan) + } + + System.out.println(); + System.out.println("Results: " + pass + " passed, " + fail + " failed"); + if (fail > 0) { + System.exit(1); + } + } +} diff --git a/defects/yabause-0001/patch/yabause-0001.patch b/defects/yabause-0001/patch/yabause-0001.patch new file mode 100644 index 000000000..b763e6815 --- /dev/null +++ b/defects/yabause-0001/patch/yabause-0001.patch @@ -0,0 +1,73 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/yabause/src/netlink.c ++++ b/yabause/src/netlink.c +@@ -579,10 +579,11 @@ static void NetlinkWriteByte(u32 addr, u8 val) + else if (NetlinkArea->connectstatus == NL_CONNECTSTATUS_LOGIN1 && + NetlinkArea->modemstate == NL_MODEMSTATE_DATA && + val == 0x0D) + { + // Internet login name + NetlinkArea->connectstatus = NL_CONNECTSTATUS_LOGIN2; +- NETLINK_LOG("login response: %s", NetlinkArea->inbuffer+NetlinkArea->inbufferstart); ++ /* Do not log the login name verbatim — credentials in cleartext (CWE-312). */ ++ NETLINK_LOG("login response: [REDACTED %d bytes]", (int)strlen(NetlinkArea->inbuffer+NetlinkArea->inbufferstart)); + NetlinkDoATResponse("\r\npassword:"); + NetlinkUpdateReceivedDataInt(); + } +@@ -588,8 +589,9 @@ static void NetlinkWriteByte(u32 addr, u8 val) + else if (NetlinkArea->connectstatus == NL_CONNECTSTATUS_LOGIN2 && + NetlinkArea->modemstate == NL_MODEMSTATE_DATA && + val == 0x0D) + { + // Internet password + NetlinkArea->connectstatus = NL_CONNECTSTATUS_LOGIN3; +- NETLINK_LOG("password response: %s", NetlinkArea->inbuffer+NetlinkArea->inbufferstart); ++ /* Do not log the password verbatim — credentials in cleartext (CWE-312). */ ++ NETLINK_LOG("password response: [REDACTED %d bytes]", (int)strlen(NetlinkArea->inbuffer+NetlinkArea->inbufferstart)); + NetlinkDoATResponse("\r\n$"); + NetlinkUpdateReceivedDataInt(); + } + +# Defect: yabause-0001 +# MOAD: 0004 (CWE-312 — Cleartext Storage of Sensitive Information) +# File: yabause/src/netlink.c +# Function: NetlinkWriteByte +# Lines: 582, 592 +# +# Description: +# The Sega Saturn NetLink modem emulation (yabause/src/netlink.c) handles +# Saturn online dial-up sessions. During the PPP/shell login handshake the +# emulator exchanges a username and password with the remote server on behalf +# of the emulated Saturn game. +# +# When NETLINK_DEBUG is defined at compile time, two NETLINK_LOG calls write +# the authentication credentials verbatim into the debug log: +# +# Line 582: NETLINK_LOG("login response: %s", inbuffer+inbufferstart) +# -- logs the Saturn internet login name as a cleartext string +# +# Line 592: NETLINK_LOG("password response: %s", inbuffer+inbufferstart) +# -- logs the Saturn internet password as a cleartext string +# +# NETLINK_LOG expands to DebugPrintf(MainLog, ...) which writes to MainLog +# (a persistent file-backed debug log, see debug.h). Any log file or terminal +# session that captures NETLINK_DEBUG output will contain the plaintext +# username and password used to authenticate the Saturn session. +# +# Severity: MEDIUM +# - Requires NETLINK_DEBUG compile-time flag (debug/developer builds) +# - Exposes Saturn internet credentials (ISP username + password) in cleartext +# - Credentials could authenticate a real ISP account if a user tests with +# real legacy credentials (e.g. Sega Net, NetLink ISP accounts) +# - Log files written to disk may persist and be included in crash reports +# +# Fix: +# Replace the verbatim credential string with a redacted placeholder that +# preserves the diagnostic byte-count without exposing credential content. +# The fix applies to both the login name log (line 582) and the password +# log (line 592). +# +# References: +# - CWE-312: Cleartext Storage of Sensitive Information +# - NETLINK_LOG macro: yabause/src/debug.h line 65 +# - NL_CONNECTSTATUS states: yabause/src/netlink.h lines 45-47 diff --git a/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$ConnectStatus.class b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$ConnectStatus.class new file mode 100644 index 000000000..9d0ecd827 Binary files /dev/null and b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$ConnectStatus.class differ diff --git a/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$NetlinkLog.class b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$NetlinkLog.class new file mode 100644 index 000000000..2f752033c Binary files /dev/null and b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$NetlinkLog.class differ diff --git a/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$TestCase.class b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$TestCase.class new file mode 100644 index 000000000..508af9bf3 Binary files /dev/null and b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest$TestCase.class differ diff --git a/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.class b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.class new file mode 100644 index 000000000..c42d8a2b2 Binary files /dev/null and b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.class differ diff --git a/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.java b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.java new file mode 100644 index 000000000..6c2558608 --- /dev/null +++ b/defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.java @@ -0,0 +1,217 @@ +import java.util.*; + +/** + * Unit test for yabause CWE-312 defect yabause-0001. + * + * yabause-0001: In yabause/src/netlink.c, NetlinkWriteByte() handles the + * Saturn NetLink modem dial-up login sequence. When NETLINK_DEBUG is + * compiled in, two NETLINK_LOG calls write the Saturn internet credentials + * verbatim into the debug log: + * + * Line 582: NETLINK_LOG("login response: %s", inbuffer+inbufferstart) + * Line 592: NETLINK_LOG("password response: %s", inbuffer+inbufferstart) + * + * This test models the C logging logic and verifies the redaction fix. + */ +public class YabauseNetlinkCredentialLogTest { + + /** Connection state machine, mirroring NL_CONNECTSTATUS_* enum. */ + enum ConnectStatus { + IDLE, LOGIN1, LOGIN2, LOGIN3, CONNECTED + } + + /** Simulates the NETLINK_LOG debug output sink. */ + static class NetlinkLog { + private final List entries = new ArrayList<>(); + + void log(String msg) { + entries.add(msg); + } + + boolean anyEntryContains(String substring) { + for (String e : entries) { + if (e.contains(substring)) return true; + } + return false; + } + + List getEntries() { + return Collections.unmodifiableList(entries); + } + } + + // --------------------------------------------------------------- + // DEFECTIVE: logs login name and password verbatim + // --------------------------------------------------------------- + + static ConnectStatus defectiveHandleCarriageReturn( + NetlinkLog log, ConnectStatus status, String inbuffer) { + + if (status == ConnectStatus.LOGIN1) { + // Line 582 — logs login name verbatim + log.log(String.format("login response: %s", inbuffer)); + return ConnectStatus.LOGIN2; + } + if (status == ConnectStatus.LOGIN2) { + // Line 592 — logs password verbatim + log.log(String.format("password response: %s", inbuffer)); + return ConnectStatus.LOGIN3; + } + if (status == ConnectStatus.LOGIN3) { + log.log(String.format("shell response: %s", inbuffer)); + return ConnectStatus.CONNECTED; + } + return status; + } + + // --------------------------------------------------------------- + // FIXED: redacts credentials, logs byte count only + // --------------------------------------------------------------- + + static ConnectStatus fixedHandleCarriageReturn( + NetlinkLog log, ConnectStatus status, String inbuffer) { + + if (status == ConnectStatus.LOGIN1) { + // Patched line 582: redacted + log.log(String.format("login response: [REDACTED %d bytes]", + inbuffer.length())); + return ConnectStatus.LOGIN2; + } + if (status == ConnectStatus.LOGIN2) { + // Patched line 592: redacted + log.log(String.format("password response: [REDACTED %d bytes]", + inbuffer.length())); + return ConnectStatus.LOGIN3; + } + if (status == ConnectStatus.LOGIN3) { + log.log(String.format("shell response: %s", inbuffer)); + return ConnectStatus.CONNECTED; + } + return status; + } + + // --------------------------------------------------------------- + // Test cases + // --------------------------------------------------------------- + + static class TestCase { + final String loginName; + final String password; + + TestCase(String loginName, String password) { + this.loginName = loginName; + this.password = password; + } + } + + static final TestCase[] CASES = { + new TestCase("saturn_user", "hunter2"), + new TestCase("netlink_player", "S3cr3tP@ss"), + new TestCase("segaNet001", "CorrectHorseBattery"), + }; + + public static void main(String[] args) { + int pass = 0; + int fail = 0; + + for (TestCase tc : CASES) { + // --- DEFECTIVE path --- + NetlinkLog defLog = new NetlinkLog(); + ConnectStatus s = ConnectStatus.LOGIN1; + + s = defectiveHandleCarriageReturn(defLog, s, tc.loginName); + s = defectiveHandleCarriageReturn(defLog, s, tc.password); + defectiveHandleCarriageReturn(defLog, s, "$ "); + + boolean defExpLogin = defLog.anyEntryContains(tc.loginName); + boolean defExpPass = defLog.anyEntryContains(tc.password); + + if (defExpLogin) { + System.out.println("PASS (defect confirmed): login name '" + + tc.loginName + "' appears in defective log"); + pass++; + } else { + System.out.println("FAIL (defect NOT confirmed): login name '" + + tc.loginName + "' missing from defective log"); + fail++; + } + + if (defExpPass) { + System.out.println("PASS (defect confirmed): password '" + + tc.password + "' appears in defective log"); + pass++; + } else { + System.out.println("FAIL (defect NOT confirmed): password '" + + tc.password + "' missing from defective log"); + fail++; + } + + // --- FIXED path --- + NetlinkLog fixLog = new NetlinkLog(); + s = ConnectStatus.LOGIN1; + + s = fixedHandleCarriageReturn(fixLog, s, tc.loginName); + s = fixedHandleCarriageReturn(fixLog, s, tc.password); + fixedHandleCarriageReturn(fixLog, s, "$ "); + + boolean fixExpLogin = fixLog.anyEntryContains(tc.loginName); + boolean fixExpPass = fixLog.anyEntryContains(tc.password); + + if (!fixExpLogin) { + System.out.println("PASS (fix confirmed): login name '" + + tc.loginName + "' NOT in fixed log"); + pass++; + } else { + System.out.println("FAIL (fix broken): login name '" + + tc.loginName + "' still appears in fixed log"); + fail++; + } + + if (!fixExpPass) { + System.out.println("PASS (fix confirmed): password '" + + tc.password + "' NOT in fixed log"); + pass++; + } else { + System.out.println("FAIL (fix broken): password '" + + tc.password + "' still appears in fixed log"); + fail++; + } + + // Redaction markers must be present + boolean hasLoginRedact = fixLog.anyEntryContains("login response: [REDACTED"); + boolean hasPassRedact = fixLog.anyEntryContains("password response: [REDACTED"); + + if (hasLoginRedact) { + System.out.println("PASS (login redaction marker present)"); + pass++; + } else { + System.out.println("FAIL (login redaction marker missing)"); + fail++; + } + + if (hasPassRedact) { + System.out.println("PASS (password redaction marker present)"); + pass++; + } else { + System.out.println("FAIL (password redaction marker missing)"); + fail++; + } + + // Shell response (non-credential) must still log verbatim + boolean shellVisible = fixLog.anyEntryContains("shell response: $ "); + if (shellVisible) { + System.out.println("PASS (non-credential shell response visible)"); + pass++; + } else { + System.out.println("FAIL (non-credential shell response missing)"); + fail++; + } + } + + System.out.println(); + System.out.println("Results: " + pass + " passed, " + fail + " failed"); + if (fail > 0) { + System.exit(1); + } + } +} diff --git a/defects/zesarux-0001/patch/zesarux-0001.patch b/defects/zesarux-0001/patch/zesarux-0001.patch new file mode 100644 index 000000000..34e27785b --- /dev/null +++ b/defects/zesarux-0001/patch/zesarux-0001.patch @@ -0,0 +1,66 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/zrcp/remote.c ++++ b/src/zrcp/remote.c +@@ -3881,7 +3881,9 @@ static void process_remote_command(int misocket, char *comando) + int longitud_comando=strlen(comando); + + if (longitud_comando entries = new ArrayList<>(); + + void log(String msg) { + entries.add(msg); + } + + List getEntries() { + return Collections.unmodifiableList(entries); + } + + boolean anyEntryContains(String substring) { + for (String e : entries) { + if (e.contains(substring)) return true; + } + return false; + } + } + + // --------------------------------------------------------------- + // DEFECTIVE: logs full command and parameter strings verbatim + // --------------------------------------------------------------- + + static void defectiveProcessCommand(DebugLog log, String command) { + int len = command.length(); + // Line 3884: logs full command including inline password + log.log(String.format("Remote command: length: %d [%s]", len, command)); + + // Split into verb + parameters (simple split on first space) + int spaceIdx = command.indexOf(' '); + String verb = (spaceIdx >= 0) ? command.substring(0, spaceIdx) : command; + String params = (spaceIdx >= 0) ? command.substring(spaceIdx + 1) : ""; + + log.log(String.format("Remote command without parameters: length: %d [%s]", + verb.length(), verb)); + + // Line 3963: logs parameters verbatim — exposes password as first token + log.log(String.format("Remote command parameters: length: %d [%s]", + params.length(), params)); + } + + // --------------------------------------------------------------- + // FIXED: logs length only, redacts parameter values + // --------------------------------------------------------------- + + static void fixedProcessCommand(DebugLog log, String command) { + int len = command.length(); + // Patched line 3884: length only, no body + log.log(String.format("Remote command: length: %d", len)); + + int spaceIdx = command.indexOf(' '); + String verb = (spaceIdx >= 0) ? command.substring(0, spaceIdx) : command; + String params = (spaceIdx >= 0) ? command.substring(spaceIdx + 1) : ""; + + log.log(String.format("Remote command without parameters: length: %d [%s]", + verb.length(), verb)); + + // Patched line 3963: parameter length + REDACTED placeholder + log.log(String.format("Remote command parameters: length: %d [REDACTED]", + params.length())); + } + + // --------------------------------------------------------------- + // ZENG commands that carry a password as first or later parameter + // --------------------------------------------------------------- + + static final String[][] CREDENTIAL_COMMANDS = { + {"authorize-join", "CREATORPASS_ALPHA 0 rw"}, + {"destroy-room", "CREATORPASS_BETA 1"}, + {"get-keys", "USERPASS_GAMMA 0"}, + {"kick", "CREATORPASS_DELTA 0 uuid-1234"}, + {"leave", "0 USERPASS_EPSILON uuid-5678"}, + {"rename-room", "CREATORPASS_ZETA 0 MyRoom"}, + {"put-snapshot", "CREATORPASS_ETA 0 DEADBEEF"}, + {"send-keys", "USERPASS_THETA 0 uuid-9999 65 1 0"}, + }; + + public static void main(String[] args) { + int pass = 0; + int fail = 0; + + for (String[] entry : CREDENTIAL_COMMANDS) { + String verb = entry[0]; + String params = entry[1]; + // Extract the password token (first token of params) + String password = params.split(" ")[0]; + String command = verb + " " + params; + + // -- DEFECTIVE: password must appear in log -- + DebugLog defLog = new DebugLog(); + defectiveProcessCommand(defLog, command); + + boolean defectiveExposes = defLog.anyEntryContains(password); + if (defectiveExposes) { + System.out.println("PASS (defect confirmed): " + verb + + " exposes '" + password + "' in log"); + pass++; + } else { + System.out.println("FAIL (defect NOT confirmed): " + verb + + " did not expose '" + password + "' — test is wrong"); + fail++; + } + + // -- FIXED: password must NOT appear in log -- + DebugLog fixLog = new DebugLog(); + fixedProcessCommand(fixLog, command); + + boolean fixedExposes = fixLog.anyEntryContains(password); + if (!fixedExposes) { + System.out.println("PASS (fix confirmed): " + verb + + " does NOT expose '" + password + "' after fix"); + pass++; + } else { + System.out.println("FAIL (fix broken): " + verb + + " still exposes '" + password + "' after fix"); + fail++; + } + + // -- FIXED: [REDACTED] placeholder must appear -- + boolean hasRedacted = fixLog.anyEntryContains("[REDACTED]"); + if (hasRedacted) { + System.out.println("PASS (redaction marker present): " + verb); + pass++; + } else { + System.out.println("FAIL (no redaction marker): " + verb); + fail++; + } + } + + System.out.println(); + System.out.println("Results: " + pass + " passed, " + fail + " failed"); + if (fail > 0) { + System.exit(1); + } + } +}