From fc7fdad2dc9e49fa5c906e2afca332ba98d0fa48 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 20:27:34 -0400 Subject: [PATCH] =?UTF-8?q?suitecrm+dolibarr:=205-MOAD=20scan=20=E2=80=94?= =?UTF-8?q?=203=20new=20defects,=20MOADs=200002/0003/0005=20CLEAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit suitecrm-0004: SugarBean.php subpanel union field dedup in_array($field, $all_fields) inside nested foreach over subpanel queries — O(S*F^2), 4.3x at S=40 F=60. Fix: parallel hash set for O(1) isset() check. dolibarr-0004: emailcollector_card.php line 575 — IMAP password logged verbatim unconditionally in dol_syslog(). CWE-312 HIGH. Fix: replace with literal ***. dolibarr-0005: functions_ldap.php line 98 — LDAP admin searchPassword first 3 chars leaked via dol_trunc() in dol_syslog() and browser print when $ldapdebug=true. CWE-312 MEDIUM. Fix: replace dol_trunc(...) with literal *** in both sinks. All 4 SuiteCRM tests PASS. All 5 Dolibarr tests PASS. MOADs 0002/0003/0005 CLEAN for both targets (PHP single-threaded, architectural globals). --- .../MOAD-0001-0002-0003-0005-partial-CLEAN.md | 42 ++++++++ ...-emailcollector-imap-password-logged.patch | 11 ++ ...barr-0005-ldap-searchpassword-logged.patch | 11 ++ .../dolibarr/test/DolibarrCWE407Test.class | Bin 5644 -> 7930 bytes defects/dolibarr/test/DolibarrCWE407Test.java | 94 +++++++++++++++++- defects/suitecrm/MOAD-0002-0003-0005-CLEAN.md | 43 ++++++++ ...rm-0004-sugarbean-allfields-in_array.patch | 19 ++++ .../suitecrm/test/SuiteCRMCWE407Test.class | Bin 5252 -> 6888 bytes defects/suitecrm/test/SuiteCRMCWE407Test.java | 84 +++++++++++++++- 9 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 defects/dolibarr/MOAD-0001-0002-0003-0005-partial-CLEAN.md create mode 100644 defects/dolibarr/patch/dolibarr-0004-emailcollector-imap-password-logged.patch create mode 100644 defects/dolibarr/patch/dolibarr-0005-ldap-searchpassword-logged.patch create mode 100644 defects/suitecrm/MOAD-0002-0003-0005-CLEAN.md create mode 100644 defects/suitecrm/patch/suitecrm-0004-sugarbean-allfields-in_array.patch diff --git a/defects/dolibarr/MOAD-0001-0002-0003-0005-partial-CLEAN.md b/defects/dolibarr/MOAD-0001-0002-0003-0005-partial-CLEAN.md new file mode 100644 index 000000000..ba8934536 --- /dev/null +++ b/defects/dolibarr/MOAD-0001-0002-0003-0005-partial-CLEAN.md @@ -0,0 +1,42 @@ +# Dolibarr — MOAD scan summary + +**Date:** 2026-03-31 +**Target:** https://github.com/Dolibarr/dolibarr (PHP ERP) +**Scanner:** Agent Blackops 5-MOAD sweep +**Note:** dolibarr-0001 through dolibarr-0005 patched. dolibarr-0004 and dolibarr-0005 are new CWE-312 defects. + +## MOAD-0001 (CWE-407): 3 defects found (pre-existing) + +- **dolibarr-0001**: `filecheck.php` — `in_array($tmprelativefilename, $file_list['insignature'])` — O(S*I), 83.5x +- **dolibarr-0002**: `datapolicycron.class.php` — `in_array($obj->rowid, $processedIds)` growing dedup — O(N^2), 23.8x +- **dolibarr-0003**: `bookkeeping.class.php` — `in_array($bookKeeping->piece_num, $alreadyExtourneT)` — O(P*E*A), 39.9x + +No new MOAD-0001 defects found in product/invoice/contact core classes. All remaining +`in_array` calls in hot paths operate on small UI selection arrays (`$arrayofselected` +from POST) or fixed-size type lists — not algorithmic hot paths. + +## MOAD-0002 (Intertangle): CLEAN (architectural PHP pattern) + +Dolibarr uses `global $conf, $db, $langs, $user` throughout. This is the framework-level +architecture — all modules share these globals intentionally. No single discrete patchable +coupling found beyond the intentional design. + +## MOAD-0003 (Leaked Context): CLEAN (standard PHP session pattern) + +User identity stored in `$_SESSION` and loaded into `global $user`. Standard PHP architecture. +No ThreadLocal misuse; PHP is single-threaded per request. + +## MOAD-0004 (CWE-312): 2 defects found + +- **dolibarr-0004**: `htdocs/admin/emailcollector_card.php` line 575 — IMAP password logged verbatim + unconditionally: `dol_syslog("... password=".$object->password...)`. Severity: HIGH. Fix: replace + `$object->password` with literal `***` in log string. +- **dolibarr-0005**: `htdocs/core/login/functions_ldap.php` line 98 — LDAP admin `searchPassword` + first 3 chars logged via `dol_trunc($ldap->searchPassword, 3)`, gated on `$ldapdebug` flag AND + also printed to browser output. Severity: MEDIUM (debug-gated). Fix: replace `dol_trunc(...)` with + literal `***` in both `dol_syslog()` and `print` calls. + +## MOAD-0005 (Thundering Herd): CLEAN + +Dolibarr uses PHP in-process caching (`$conf->cache_*` arrays). PHP single-threaded per +request. No distributed atomic cache patterns needed. No get+null+compute+set race possible. diff --git a/defects/dolibarr/patch/dolibarr-0004-emailcollector-imap-password-logged.patch b/defects/dolibarr/patch/dolibarr-0004-emailcollector-imap-password-logged.patch new file mode 100644 index 000000000..4ce5a9bb9 --- /dev/null +++ b/defects/dolibarr/patch/dolibarr-0004-emailcollector-imap-password-logged.patch @@ -0,0 +1,11 @@ +--- a/htdocs/admin/emailcollector_card.php ++++ b/htdocs/admin/emailcollector_card.php +@@ -572,7 +572,7 @@ + $timeoutconnect = getDolGlobalInt('MAIN_USE_CONNECT_TIMEOUT', 5); + $timeoutread = getDolGlobalInt('MAIN_USE_RESPONSE_TIMEOUT', 20); + +- dol_syslog("imap_open connectstring=".$connectstringsource." login=".$object->login." password=".$object->password." timeoutconnect=".$timeoutconnect." timeoutread=".$timeoutread); ++ dol_syslog("imap_open connectstring=".$connectstringsource." login=".$object->login." password=*** timeoutconnect=".$timeoutconnect." timeoutread=".$timeoutread); + + $result1 = imap_timeout(IMAP_OPENTIMEOUT, $timeoutconnect); // timeout seems ignored with ssl connect + $result2 = imap_timeout(IMAP_READTIMEOUT, $timeoutread); diff --git a/defects/dolibarr/patch/dolibarr-0005-ldap-searchpassword-logged.patch b/defects/dolibarr/patch/dolibarr-0005-ldap-searchpassword-logged.patch new file mode 100644 index 000000000..125934e35 --- /dev/null +++ b/defects/dolibarr/patch/dolibarr-0005-ldap-searchpassword-logged.patch @@ -0,0 +1,11 @@ +--- a/htdocs/core/login/functions_ldap.php ++++ b/htdocs/core/login/functions_ldap.php +@@ -95,9 +95,9 @@ + if ($ldapdebug) { + dol_syslog("functions_ldap::check_user_password_ldap Server:".implode(',', $ldap->server).", Port:".$ldap->serverPort.", Protocol:".$ldap->ldapProtocolVersion.", Type:".$ldap->serverType); +- dol_syslog("functions_ldap::check_user_password_ldap uid/samaccountname=".$ldapuserattr.", dn=".$ldapdn.", Admin:".$ldap->searchUser.", Pass:".dol_trunc($ldap->searchPassword, 3)); ++ dol_syslog("functions_ldap::check_user_password_ldap uid/samaccountname=".$ldapuserattr.", dn=".$ldapdn.", Admin:".$ldap->searchUser.", Pass:***"); + print "DEBUG: Server:".implode(',', $ldap->server).", Port:".$ldap->serverPort.", Protocol:".$ldap->ldapProtocolVersion.", Type:".$ldap->serverType."
\n"; +- print "DEBUG: uid/samaccountname=".$ldapuserattr.", dn=".$ldapdn.", Admin:".$ldap->searchUser.", Pass:".dol_trunc($ldap->searchPassword, 3)."
\n"; ++ print "DEBUG: uid/samaccountname=".$ldapuserattr.", dn=".$ldapdn.", Admin:".$ldap->searchUser.", Pass:***
\n"; + } diff --git a/defects/dolibarr/test/DolibarrCWE407Test.class b/defects/dolibarr/test/DolibarrCWE407Test.class index daaa80331df026b674c6df7e5727eb7182211b10..5eafc5cb034137c6e2d3a9f5cb8fc6e3aeaf2904 100644 GIT binary patch delta 3503 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 V$DR0v6HlpKv?&H(so~2M{|n5R5vl+H diff --git a/defects/dolibarr/test/DolibarrCWE407Test.java b/defects/dolibarr/test/DolibarrCWE407Test.java index d3ead1afb..0227012a6 100644 --- a/defects/dolibarr/test/DolibarrCWE407Test.java +++ b/defects/dolibarr/test/DolibarrCWE407Test.java @@ -1,7 +1,7 @@ import java.util.*; /** - * CWE-407 unit tests for Dolibarr defects. + * Unit tests for Dolibarr defects (CWE-407 and CWE-312). * * dolibarr-0001: filecheck.php in_array($tmprelativefilename, $file_list['insignature']) * — O(S*I) file membership check during integrity verification @@ -9,6 +9,10 @@ import java.util.*; * — O(N^2) dedup accumulator in GDPR data policy cron * dolibarr-0003: bookkeeping.class.php in_array($bookKeeping->piece_num, $alreadyExtourneT) * — O(P*E*A) reversal check in accounting journal extourne + * dolibarr-0004: emailcollector_card.php dol_syslog(...password=$object->password...) + * — CWE-312: IMAP password logged verbatim to system log + * dolibarr-0005: functions_ldap.php dol_syslog(...Pass:dol_trunc($ldap->searchPassword, 3)) + * — CWE-312: LDAP admin searchPassword first 3 chars logged (debug-gated but still leaks) */ public class DolibarrCWE407Test { @@ -208,10 +212,96 @@ public class DolibarrCWE407Test { System.out.println(" PASS"); } + // ---- dolibarr-0004: IMAP password logged verbatim (CWE-312) ---- + + /** + * Models the log message builder for emailcollector_card.php line 575. + * Unpatched: concatenates $object->password verbatim into the log string. + * Patched: replaces password with literal "***". + */ + static String imapLogUnpatched(String connectstring, String login, String password) { + return "imap_open connectstring=" + connectstring + + " login=" + login + + " password=" + password; + } + + static String imapLogPatched(String connectstring, String login, String password) { + return "imap_open connectstring=" + connectstring + + " login=" + login + + " password=***"; + } + + // ---- dolibarr-0005: LDAP searchPassword first 3 chars logged (CWE-312) ---- + + /** Simulates dol_trunc(password, 3): returns first 3 chars + "..." */ + static String dolTrunc(String s, int n) { + if (s == null || s.length() <= n) return s; + return s.substring(0, n) + "..."; + } + + static String ldapLogUnpatched(String searchUser, String searchPassword) { + return "Admin:" + searchUser + ", Pass:" + dolTrunc(searchPassword, 3); + } + + static String ldapLogPatched(String searchUser, String searchPassword) { + return "Admin:" + searchUser + ", Pass:***"; + } + + static void testDolibarr0004() { + System.out.println("=== dolibarr-0004: IMAP password verbatim in log (CWE-312) ==="); + String connectstring = "{imap.example.com:993/imap/ssl}INBOX"; + String login = "user@example.com"; + String password = "S3cr3tP@ss!"; + + String unpatched = imapLogUnpatched(connectstring, login, password); + String patched = imapLogPatched(connectstring, login, password); + + // Unpatched log contains the real password + assert unpatched.contains(password) : "Expected unpatched log to contain password"; + + // Patched log must NOT contain the real password + assert !patched.contains(password) : "Patched log must not contain password"; + assert patched.contains("***") : "Patched log must contain redaction marker"; + + // Both logs contain the login (not a secret) + assert unpatched.contains(login) : "Log must contain login"; + assert patched.contains(login) : "Log must contain login"; + + System.out.println(" unpatched log: " + unpatched); + System.out.println(" patched log: " + patched); + System.out.println(" PASS"); + } + + static void testDolibarr0005() { + System.out.println("=== dolibarr-0005: LDAP searchPassword partial leak in log (CWE-312) ==="); + String searchUser = "cn=ldapadmin,dc=example,dc=com"; + String searchPassword = "S3cr3tAdminPass"; + + String unpatched = ldapLogUnpatched(searchUser, searchPassword); + String patched = ldapLogPatched(searchUser, searchPassword); + + // Unpatched log leaks first 3 chars of password + String leaked = searchPassword.substring(0, 3); + assert unpatched.contains(leaked) : "Unpatched log should contain first 3 chars: " + leaked; + + // Patched log must NOT contain any prefix of the password + assert !patched.contains(leaked) : "Patched log must not leak any password chars"; + assert patched.contains("***") : "Patched log must contain redaction marker"; + + // Both logs contain the admin DN (not a secret per se, but identity) + assert patched.contains(searchUser) : "Log should still contain admin user DN"; + + System.out.println(" unpatched log: " + unpatched); + System.out.println(" patched log: " + patched); + System.out.println(" PASS"); + } + public static void main(String[] args) { testDolibarr0001(); testDolibarr0002(); testDolibarr0003(); - System.out.println("\nAll 3 Dolibarr CWE-407 tests PASSED."); + testDolibarr0004(); + testDolibarr0005(); + System.out.println("\nAll 5 Dolibarr tests PASSED."); } } diff --git a/defects/suitecrm/MOAD-0002-0003-0005-CLEAN.md b/defects/suitecrm/MOAD-0002-0003-0005-CLEAN.md new file mode 100644 index 000000000..4a905a199 --- /dev/null +++ b/defects/suitecrm/MOAD-0002-0003-0005-CLEAN.md @@ -0,0 +1,43 @@ +# SuiteCRM — MOADs 0002, 0003, 0005 CLEAN + +**Date:** 2026-03-31 +**Target:** https://github.com/salesagility/SuiteCRM (PHP CRM) +**Scanner:** Agent Blackops 5-MOAD sweep +**Note:** suitecrm-0001 through suitecrm-0004 are CWE-407 patches. suitecrm-0004 is new (SugarBean subpanel field dedup). + +## MOAD-0001 (CWE-407): 4 defects found + +- **suitecrm-0001**: `InboundEmail.php` — `in_array($overview->imap_uid, $uids)` — O(I*U) +- **suitecrm-0002**: `jjwg_Maps/controller.php` — `in_array($display['id'], $records)` — O(D*R) +- **suitecrm-0003**: `Email.php` — `in_array(trim($match[0]), $knownEmails)` growing dedup — O(N^2) +- **suitecrm-0004**: `data/SugarBean.php` — `in_array($field, $all_fields)` inside nested foreach over subpanels — O(S*F^2), 4.3x at S=40 F=60 + +## MOAD-0002 (Intertangle): CLEAN (architectural, no patch target) + +SuiteCRM uses `$GLOBALS['current_user']`, `$GLOBALS['db']`, and `$GLOBALS['sugar_config']` as +shared mutable global state throughout 101+ modules. This is the standard SugarCRM/SuiteCRM +architectural pattern, not a single patchable coupling point. The global state is framework-level +design. No single god object coupling stands out as a discrete patchable defect beyond the +framework's intentional design. + +## MOAD-0003 (Leaked Context): CLEAN (architectural PHP pattern) + +SuiteCRM stores `authenticated_user_id` in `$_SESSION` (PHP standard). The user object is +then stored in `$GLOBALS['current_user']`. This is the standard PHP session architecture. +No ThreadLocal misuse (PHP is not thread-based). No ScopedValue equivalent needed here. + +## MOAD-0004 (CWE-312): CLEAN + +LDAP authentication (`LDAPAuthenticateUser.php`) logs `$bind_user` (username DN) and +`$admin_user` (admin bind DN) but never logs `$bind_password` or `$admin_password`. +The admin password is fetched from config and used directly in `ldap_bind()` without +being interpolated into any log string. SMTP/outbound email logs HOST and PORT only, +never SMTP password. InboundEmail IMAP logs connection parameters without password. + +## MOAD-0005 (Thundering Herd): CLEAN + +`SugarCache` (`include/SugarCache/SugarCache.php`) is an in-process PHP array cache. +PHP is single-threaded per request — no concurrent access races possible. The +get+null+compute+set pattern is safe in this single-threaded context. `sugar_cache_put` +is not a distributed cache without additional configuration, so no thundering herd risk +in default deployments. diff --git a/defects/suitecrm/patch/suitecrm-0004-sugarbean-allfields-in_array.patch b/defects/suitecrm/patch/suitecrm-0004-sugarbean-allfields-in_array.patch new file mode 100644 index 000000000..4c236102a --- /dev/null +++ b/defects/suitecrm/patch/suitecrm-0004-sugarbean-allfields-in_array.patch @@ -0,0 +1,19 @@ +--- a/data/SugarBean.php ++++ b/data/SugarBean.php +@@ -883,12 +883,13 @@ + //If final_query is still empty, its time to build the sub-queries + if (empty($final_query)) { + $subqueries = SugarBean::build_sub_queries_for_union($subpanel_list, $subpanel_def, $parentbean, $order_by); + $all_fields = array(); ++ $all_fields_set = array(); // O(1) lookup instead of O(N) in_array + foreach ($subqueries as $i => $subquery) { + $query_fields = DBManagerFactory::getInstance()->getSelectFieldsFromQuery($subquery['select']); + foreach ($query_fields as $field => $select) { +- if (!in_array($field, $all_fields)) { ++ if (!isset($all_fields_set[$field])) { + $all_fields[] = $field; ++ $all_fields_set[$field] = true; + } + } + $subqueries[$i]['query_fields'] = $query_fields; + } diff --git a/defects/suitecrm/test/SuiteCRMCWE407Test.class b/defects/suitecrm/test/SuiteCRMCWE407Test.class index 059985701702d51a22607e91c71ac37cde0c7221..57bce18ad408024740c75eb5bf40c14470b36f71 100644 GIT binary patch delta 2847 zcmaJ@3vg7`8UD_`cXN|$mTZ#E%Ox62NCGhgC>w$yJd{d+A=os9X}Tqsu(+Fq>@H2w z&g#gFt*uj>tMX8a4Qi^bJQ53=M0r*5Q82WktqMM=g4)`dq9aI1zjJpZ9buZ8oOAAf z&iT*x{r`9Vvl~j!Om)0+X>dP)+4y{q<1I_fh6e?v*4EZ~!_h!QUD*|?sI084_IVql zZT_x$)!)vEA9woO)u4A(Kn=EfTh-QRre1Trf^`rDqt)rUErd5xeInIx8X^Fdat*!wz${PHz`^@5L*`uHj76E5O4M!Sfpyn)XV4SMts%I4Z@oJM( z$I7wVrgX_;#pfYF}HgGtR0hmA!V0 ztxb7Vz#I*G8w!L&G6g=b@U9Dcx#>NDYvy%# zsw*RED~*YeCe#ux_qKIJG@s3?ag9#}Ck(s8*Y33(rR$W1JSK^BGPfx2CF{zt&-WcE>p7nDm_yy1VB|KDy zm)fXcKLx*L?l+V9t>j#-o|6X zf*r!a!d-$VL?wE4xKm*r<+0jV=~Y&iUwP;IZ*b-(0IlIQ2A(3I-xJUu2xun(?INJv z1hk8Q`Ut2W^Y9D<(oaxvf*Qms3^5+hB8vSC*8xW6An6%HJdTgZ=3}BggLl|}fU^SlgjMuY9ZM;! z%p=Q|Ohr|Rc^C0vj=XE$!#9QLqI}+y%^^)6-d*jG1CUK=pB8vT;4`0(G5ls>mA)$ z9>?aUIBu@z;1Qphqw(r=OVY5}XG<9DeOSB|6Fp{+Z2;S))(eNw*etI>58ntH*$q-4 z*oUSA&dl6AXJ+f_A5BZq_Z27Cxl^{oBlo#ebW2R`=}qj(vfP_xzqezCBI*CmX?YHY zW@By~Esf2lJiXJeACJ*LELoOJsG&I88k? zlg-8pl8Qze6&6>Kyr7twJ=9EZ*)-Mk&|8nqI7R*Uh8c#&sh4l9zTf#iY&%r4!)2#eWM_tUyL5V0c4l6e%X@?k$xX4wrd7>#O|iS#1t zY_rdh=Ty|!FD&*B**TGJ4phL zrh1PVQkolM>rHuVLv6#l`dHC3FE-eG?p<^iq|6C(!r>&REXG~RIqh{Meiu5cO}+8N zys*VNL$*G%y&*U2CKqi+Sg}VrU8UBjO;~&s)2|Q12a5hMBw_O)_Yg;q7fz~J=8w$R z7^JCht`?`0&pfW(N$oF#_4{#^dGt3kV z+fR1!OtQUN**?wuc^0!qBdc(DNX&=Dd_>Gg#e7W6$7eBv1N;|r=Ts347Ny>0T7A(M zSfKcnptZO>O){%>sWIKn8l7p3VxAMcPP%J(LT#;9Jj*)jd5K2W^BPUONi&;xnRnPA zfmb-E-fw8(BCne;Eym#ulTWLOYNlTM}jyS#4*ljuA_Mqd?BsOe*i_#1WW(` diff --git a/defects/suitecrm/test/SuiteCRMCWE407Test.java b/defects/suitecrm/test/SuiteCRMCWE407Test.java index e361a2a79..83e6a4f1d 100644 --- a/defects/suitecrm/test/SuiteCRMCWE407Test.java +++ b/defects/suitecrm/test/SuiteCRMCWE407Test.java @@ -9,6 +9,8 @@ import java.util.*; * — O(D*R) map marker record filter * suitecrm-0003: Email.php in_array(trim($match[0]), $knownEmails) * — O(N^2) email dedup accumulator + * suitecrm-0004: SugarBean.php in_array($field, $all_fields) inside nested foreach + * — O(S*F^2) subpanel union field dedup during subpanel list view */ public class SuiteCRMCWE407Test { @@ -90,6 +92,43 @@ public class SuiteCRMCWE407Test { return unique; } + // ---- suitecrm-0004: SugarBean subpanel union field dedup ---- + + /** + * Unpatched: in_array($field, $all_fields) inside nested foreach over subpanels * fields. + * $all_fields grows each time a new unique field is appended. + * Complexity: O(S * F * all_fields_size) = O(S * F^2) worst case. + */ + static List subpanelFieldDedupUnpatched(List> subpanelFields) { + List allFields = new ArrayList<>(); + for (List fields : subpanelFields) { + for (String field : fields) { + if (!allFields.contains(field)) { // O(allFields) scan + allFields.add(field); + } + } + } + return allFields; + } + + /** + * Patched: isset($all_fields_set[$field]) — O(1) lookup per field. + * Complexity: O(S * F) total. + */ + static List subpanelFieldDedupPatched(List> subpanelFields) { + List allFields = new ArrayList<>(); + Set allFieldsSet = new HashSet<>(); // O(1) lookup + for (List fields : subpanelFields) { + for (String field : fields) { + if (!allFieldsSet.contains(field)) { // O(1) + allFields.add(field); + allFieldsSet.add(field); + } + } + } + return allFields; + } + // ---- Test harness ---- static void testSuiteCRM0001() { @@ -195,10 +234,53 @@ public class SuiteCRMCWE407Test { System.out.println(" PASS"); } + static void testSuiteCRM0004() { + System.out.println("=== suitecrm-0004: SugarBean subpanel field dedup ==="); + int S = 40; // subpanels in a busy Account detail view + int F = 60; // fields per subpanel query + + // Build subpanel field lists with overlapping fields across subpanels + List> subpanelFields = new ArrayList<>(); + for (int s = 0; s < S; s++) { + List fields = new ArrayList<>(); + for (int f = 0; f < F; f++) { + // First 20 fields are shared (common: id, name, date_entered, etc.) + if (f < 20) { + fields.add("common_field_" + f); + } else { + fields.add("subpanel_" + s + "_field_" + f); + } + } + subpanelFields.add(fields); + } + + // Warmup + subpanelFieldDedupPatched(subpanelFields); + + long t0 = System.nanoTime(); + List r1 = subpanelFieldDedupUnpatched(subpanelFields); + long unpatched = System.nanoTime() - t0; + + t0 = System.nanoTime(); + List r2 = subpanelFieldDedupPatched(subpanelFields); + long patched = System.nanoTime() - t0; + + assert r1.equals(r2) : "Result mismatch: " + r1.size() + " vs " + r2.size(); + int expectedUnique = 20 + S * (F - 20); // 20 common + S*40 unique per subpanel + assert r1.size() == expectedUnique : "Expected " + expectedUnique + " unique fields, got " + r1.size(); + + double ratio = (double) unpatched / patched; + System.out.printf(" S=%d F=%d unique=%d unpatched=%dms patched=%dms ratio=%.1fx%n", + S, F, r1.size(), unpatched / 1_000_000, patched / 1_000_000, ratio); + assert ratio > 3.0 : "Expected >3x ratio, got " + ratio; + System.out.println(" PASS"); + } + public static void main(String[] args) { testSuiteCRM0001(); testSuiteCRM0002(); testSuiteCRM0003(); - System.out.println("\nAll 3 SuiteCRM CWE-407 tests PASSED."); + testSuiteCRM0004(); + System.out.println("\nAll 4 SuiteCRM CWE-407 tests PASSED."); } }