From 6237a8d37d985af3cc0164a72d6b108c0107cbf5 Mon Sep 17 00:00:00 2001 From: zapbot <12745184+zapbot@users.noreply.github.com> Date: Thu, 16 Oct 2025 06:42:56 +0000 Subject: [PATCH 1/3] Update localized resources Update resources from Crowdin. Signed-off-by: zapbot <12745184+zapbot@users.noreply.github.com> --- zap/src/main/dist/lang/Messages_ja_JP.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zap/src/main/dist/lang/Messages_ja_JP.properties b/zap/src/main/dist/lang/Messages_ja_JP.properties index 798ead576aa..1f66d5745ae 100644 --- a/zap/src/main/dist/lang/Messages_ja_JP.properties +++ b/zap/src/main/dist/lang/Messages_ja_JP.properties @@ -1930,7 +1930,7 @@ manReq.checkBox.fixLength = Content-Length \u3092\u66f4\u65b0 manReq.checkBox.followRedirect = \u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u3092\u8ffd\u8de1 manReq.checkBox.useCSRF = Anti-CSRF\u30c8\u30fc\u30af\u30f3\u3092\u518d\u751f\u6210 manReq.checkBox.useCookies = Cookie\u3092\u53d7\u3051\u5165\u308c\u308b -manReq.checkBox.useSession = \u73fe\u5728\u306e\u8ffd\u8de1\u30bb\u30c3\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b +manReq.checkBox.useSession = \u73fe\u5728\u306e\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u30bb\u30c3\u30b7\u30e7\u30f3\u3092\u4f7f\u7528 manReq.dialog.title = \u624b\u52d5\u30ea\u30af\u30a8\u30b9\u30c8\u30a8\u30c7\u30a3\u30bf\u30fc manReq.display.above = \u30ea\u30af\u30a8\u30b9\u30c8\u3092\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u524d\u306b\u8868\u793a manReq.display.sidebyside = \u30ea\u30af\u30a8\u30b9\u30c8\u3068\u30ec\u30b9\u30dd\u30f3\u30b9\u3092\u4e26\u3079\u3066\u8868\u793a From 1421f7e5298fae246bfb46af7ea2be0e2433e163 Mon Sep 17 00:00:00 2001 From: Simon Bennetts Date: Tue, 16 Sep 2025 16:50:24 +0100 Subject: [PATCH 2/3] Alert tree de-duplification Signed-off-by: Simon Bennetts --- zap/gradle/japicmp.yaml | 4 + zap/src/main/dist/db/hsqldb.properties | 6 +- zap/src/main/dist/db/mysql.properties | 5 +- zap/src/main/dist/db/mysql.schema | 1 + .../parosproxy/paros/core/scanner/Alert.java | 60 ++- .../org/parosproxy/paros/db/RecordAlert.java | 24 +- .../org/parosproxy/paros/db/TableAlert.java | 6 +- .../paros/db/paros/ParosTableAlert.java | 30 +- .../paros/model/HistoryReference.java | 39 +- .../org/parosproxy/paros/model/SiteNode.java | 73 +-- .../org/zaproxy/zap/db/sql/SqlTableAlert.java | 16 +- .../zaproxy/zap/extension/alert/AlertAPI.java | 15 +- .../zap/extension/alert/AlertNode.java | 28 +- .../zap/extension/alert/AlertPanel.java | 55 +-- .../zaproxy/zap/extension/alert/AlertSet.java | 200 +++++++++ .../alert/AlertTreeCellRenderer.java | 5 +- .../zap/extension/alert/AlertTreeModel.java | 85 ++-- .../zap/extension/alert/AlertViewPanel.java | 34 +- .../zap/extension/alert/ExtensionAlert.java | 102 +++-- .../zap/extension/alert/TextAlertTree.java | 64 +++ .../org/zaproxy/zap/resources/zapdb.script | 2 +- .../paros/core/scanner/AlertUnitTest.java | 4 + .../zap/extension/alert/AlertSetUnitTest.java | 283 ++++++++++++ .../alert/AlertTreeModelUnitTest.java | 420 ++++++++++++++++++ .../alert/ExtensionAlertUnitTest.java | 31 +- 25 files changed, 1362 insertions(+), 230 deletions(-) create mode 100644 zap/src/main/java/org/zaproxy/zap/extension/alert/AlertSet.java create mode 100644 zap/src/main/java/org/zaproxy/zap/extension/alert/TextAlertTree.java create mode 100644 zap/src/test/java/org/zaproxy/zap/extension/alert/AlertSetUnitTest.java create mode 100644 zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java diff --git a/zap/gradle/japicmp.yaml b/zap/gradle/japicmp.yaml index a441996f8a2..c151f02b0f5 100644 --- a/zap/gradle/japicmp.yaml +++ b/zap/gradle/japicmp.yaml @@ -7,6 +7,10 @@ fieldExcludes: [] classExcludes: - "org.parosproxy.paros.core.scanner.VariantAbstractRPCQuery$RPCParameter" - "org.parosproxy.paros.core.scanner.VariantJSONQuery$SimpleStringReader" + - "org.parosproxy.paros.db.RecordAlert" + - "org.parosproxy.paros.db.TableAlert" + - "org.parosproxy.paros.db.paros.ParosTableAlert" + - "org.zaproxy.zap.db.sql.SqlTableAlert" methodExcludes: - "org.zaproxy.zap.model.SessionStructure#addPath(org.parosproxy.paros.model.Session,org.parosproxy.paros.model.HistoryReference,org.parosproxy.paros.network.HttpMessage)" - "org.zaproxy.zap.model.SessionStructure#addPath(org.parosproxy.paros.model.Session,org.parosproxy.paros.model.HistoryReference,org.parosproxy.paros.network.HttpMessage,boolean)" diff --git a/zap/src/main/dist/db/hsqldb.properties b/zap/src/main/dist/db/hsqldb.properties index 79711c735e6..21f575e648e 100644 --- a/zap/src/main/dist/db/hsqldb.properties +++ b/zap/src/main/dist/db/hsqldb.properties @@ -8,6 +8,7 @@ alert.field.description = DESCRIPTION alert.field.evidence = EVIDENCE alert.field.historyid = HISTORYID alert.field.inputvector = INPUT_VECTOR +alert.field.nodename = NODENAME alert.field.otherinfo = OTHERINFO alert.field.param = PARAM alert.field.pluginid = PLUGINID @@ -26,6 +27,7 @@ alert.ps.addattack = ALTER TABLE ALERT ADD COLUMN ATTACK VARCHAR(32768) DEFAULT alert.ps.addcweid = ALTER TABLE ALERT ADD COLUMN CWEID INT DEFAULT -1 alert.ps.addevidence = ALTER TABLE ALERT ADD COLUMN EVIDENCE VARCHAR(16777216) DEFAULT '' alert.ps.addinputvector = ALTER TABLE ALERT ADD COLUMN INPUT_VECTOR VARCHAR(256) DEFAULT '' +alert.ps.addnodename = ALTER TABLE ALERT ADD COLUMN NODENAME VARCHAR(1048576) DEFAULT NULL alert.ps.addsourcehistoryid = ALTER TABLE ALERT ADD COLUMN SOURCEHISTORYID INT DEFAULT 0 alert.ps.addsourceid = ALTER TABLE ALERT ADD COLUMN SOURCEID INT DEFAULT 0 alert.ps.addsourceidindex = CREATE INDEX INDEX_ALERT_SOURCEID ON ALERT (SOURCEID) @@ -35,10 +37,10 @@ alert.ps.deleteall = DELETE FROM ALERT alert.ps.getalertsforhistoryid = SELECT * FROM ALERT WHERE SOURCEHISTORYID = ? alert.ps.getalertsforsession = SELECT ALERTID FROM ALERT INNER JOIN SCAN ON ALERT.SCANID = SCAN.SCANID WHERE SESSIONID = ? alert.ps.getallalertids = SELECT ALERTID FROM ALERT -alert.ps.insert = INSERT INTO ALERT (SCANID, PLUGINID, ALERT, RISK, RELIABILITY, DESCRIPTION, URI, PARAM, ATTACK, OTHERINFO, SOLUTION, REFERENCE, EVIDENCE, CWEID, WASCID, HISTORYID, SOURCEHISTORYID, SOURCEID, ALERTREF, INPUT_VECTOR) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +alert.ps.insert = INSERT INTO ALERT (SCANID, PLUGINID, ALERT, RISK, RELIABILITY, DESCRIPTION, URI, PARAM, ATTACK, OTHERINFO, SOLUTION, REFERENCE, EVIDENCE, CWEID, WASCID, HISTORYID, SOURCEHISTORYID, SOURCEID, ALERTREF, INPUT_VECTOR, NODENAME) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) alert.ps.lastinsert = CALL IDENTITY() alert.ps.read = SELECT TOP 1 * FROM ALERT WHERE ALERTID = ? -alert.ps.update = UPDATE ALERT SET ALERT = ?, RISK = ?, RELIABILITY = ?, DESCRIPTION = ?, URI = ?, PARAM = ?, ATTACK = ?,OTHERINFO = ?, SOLUTION = ?, REFERENCE = ?, EVIDENCE = ?, CWEID = ?, WASCID = ?, SOURCEHISTORYID = ?, INPUT_VECTOR = ? WHERE ALERTID = ? +alert.ps.update = UPDATE ALERT SET ALERT = ?, RISK = ?, RELIABILITY = ?, DESCRIPTION = ?, URI = ?, PARAM = ?, ATTACK = ?,OTHERINFO = ?, SOLUTION = ?, REFERENCE = ?, EVIDENCE = ?, CWEID = ?, WASCID = ?, SOURCEHISTORYID = ?, INPUT_VECTOR = ?, NODENAME = ? WHERE ALERTID = ? alert.ps.updatehistoryid = UPDATE ALERT SET HISTORYID = ?, SOURCEHISTORYID = ? WHERE ALERTID = ? alert.table_name = ALERT diff --git a/zap/src/main/dist/db/mysql.properties b/zap/src/main/dist/db/mysql.properties index f0d9844721b..574b8e7d182 100644 --- a/zap/src/main/dist/db/mysql.properties +++ b/zap/src/main/dist/db/mysql.properties @@ -26,6 +26,7 @@ alert.ps.addattack = ALTER TABLE ALERT ADD COLUMN ATTACK VARCHAR(32768) DEFAULT alert.ps.addcweid = ALTER TABLE ALERT ADD COLUMN CWEID INT DEFAULT -1 alert.ps.addevidence = ALTER TABLE ALERT ADD COLUMN EVIDENCE VARCHAR(16777216) DEFAULT '' alert.ps.addinputvector = ALTER TABLE ALERT ADD COLUMN INPUT_VECTOR VARCHAR(256) DEFAULT '' +alert.ps.addnodename = ALTER TABLE ALERT ADD COLUMN NODENAME VARCHAR(1048576) DEFAULT NULL alert.ps.addsourcehistoryid = ALTER TABLE ALERT ADD COLUMN SOURCEHISTORYID INT DEFAULT 0 alert.ps.addsourceid = ALTER TABLE ALERT ADD COLUMN SOURCEID INT DEFAULT 0 alert.ps.addsourceidindex = CREATE INDEX INDEX_ALERT_SOURCEID ON ALERT (SOURCEID) @@ -35,10 +36,10 @@ alert.ps.deleteall = DELETE FROM ALERT alert.ps.getalertsforhistoryid = SELECT * FROM ALERT WHERE SOURCEHISTORYID = ? alert.ps.getalertsforsession = SELECT ALERTID FROM ALERT INNER JOIN SCAN ON ALERT.SCANID = SCAN.SCANID WHERE SESSIONID = ? alert.ps.getallalertids = SELECT ALERTID FROM ALERT -alert.ps.insert = INSERT INTO ALERT (SCANID, PLUGINID, ALERT, RISK, RELIABILITY, DESCRIPTION, URI, PARAM, ATTACK, OTHERINFO, SOLUTION, REFERENCE, EVIDENCE, CWEID, WASCID, HISTORYID, SOURCEHISTORYID, SOURCEID, ALERTREF, INPUT_VECTOR) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +alert.ps.insert = INSERT INTO ALERT (SCANID, PLUGINID, ALERT, RISK, RELIABILITY, DESCRIPTION, URI, PARAM, ATTACK, OTHERINFO, SOLUTION, REFERENCE, EVIDENCE, CWEID, WASCID, HISTORYID, SOURCEHISTORYID, SOURCEID, ALERTREF, INPUT_VECTOR, NODENAME) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) alert.ps.lastinsert = SELECT LAST_INSERT_ID() alert.ps.read = SELECT * FROM ALERT WHERE ALERTID = ? LIMIT 1 -alert.ps.update = UPDATE ALERT SET ALERT = ?, RISK = ?, RELIABILITY = ?, DESCRIPTION = ?, URI = ?, PARAM = ?, ATTACK = ?,OTHERINFO = ?, SOLUTION = ?, REFERENCE = ?, EVIDENCE = ?, CWEID = ?, WASCID = ?, SOURCEHISTORYID = ?, INPUT_VECTOR = ? WHERE ALERTID = ? +alert.ps.update = UPDATE ALERT SET ALERT = ?, RISK = ?, RELIABILITY = ?, DESCRIPTION = ?, URI = ?, PARAM = ?, ATTACK = ?,OTHERINFO = ?, SOLUTION = ?, REFERENCE = ?, EVIDENCE = ?, CWEID = ?, WASCID = ?, SOURCEHISTORYID = ?, INPUT_VECTOR = ?, NODENAME = ? WHERE ALERTID = ? alert.ps.updatehistoryid = UPDATE ALERT SET HISTORYID = ?, SOURCEHISTORYID = ? WHERE ALERTID = ? alert.table_name = ALERT diff --git a/zap/src/main/dist/db/mysql.schema b/zap/src/main/dist/db/mysql.schema index df148374389..e1724ce2975 100644 --- a/zap/src/main/dist/db/mysql.schema +++ b/zap/src/main/dist/db/mysql.schema @@ -44,6 +44,7 @@ CREATE TABLE `ALERT` ( `SOURCEID` int(11) DEFAULT '0', `ALERTREF` varchar(256) DEFAULT '''', `INPUT_VECTOR` varchar(256) DEFAULT '', + `NODENAME` longtext, PRIMARY KEY (`ALERTID`), KEY `ALERT_INDEX` (`SOURCEHISTORYID`) ) ENGINE=InnoDB AUTO_INCREMENT=9436 DEFAULT CHARSET=latin1; diff --git a/zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java b/zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java index 2302a64f322..6fdc287d210 100644 --- a/zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java +++ b/zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java @@ -68,6 +68,7 @@ // ZAP: 2023/01/10 Tidy up logger. // ZAP: 2023/09/12 Add NUMBER_RISKS convenience constant. // ZAP: 2023/11/14 When setting CWE also add a CWE alert tag with an appropriate URL. +// ZAP: 2025/10/01 Added support for nodeName. package org.parosproxy.paros.core.scanner; import java.net.URL; @@ -76,6 +77,7 @@ import java.util.Map; import javax.swing.ImageIcon; import org.apache.commons.httpclient.URI; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -227,6 +229,7 @@ public static Source getSource(int id) { private URI msgUri = null; private Source source = Source.UNKNOWN; private String alertRef = ""; + private String nodeName; private Map tags = Collections.emptyMap(); public Alert(int pluginId) { @@ -289,6 +292,7 @@ private void init(RecordAlert recordAlert, HistoryReference ref) { if (alertRef != null) { this.setAlertRef(alertRef); } + this.setNodeName(recordAlert.getNodeName()); } public Alert(RecordAlert recordAlert, HistoryReference ref) { @@ -438,6 +442,14 @@ public void setMessage(HttpMessage message) { @Override public int compareTo(Alert alert2) { + if (alert2 == null) { + return 1; + } + + if (this.alertId == alert2.getAlertId()) { + return 0; + } + if (risk < alert2.risk) { return -1; } else if (risk > alert2.risk) { @@ -471,8 +483,11 @@ public int compareTo(Alert alert2) { return result; } - // ZAP: changed to compare the field uri with alert2.uri - result = uri.compareToIgnoreCase(alert2.uri); + if (StringUtils.isNotBlank(nodeName) && StringUtils.isNotBlank(alert2.nodeName)) { + result = nodeName.compareToIgnoreCase(alert2.nodeName); + } else { + result = uri.compareToIgnoreCase(alert2.uri); + } if (result != 0) { return result; } @@ -483,11 +498,6 @@ public int compareTo(Alert alert2) { return result; } - result = otherInfo.compareToIgnoreCase(alert2.otherInfo); - if (result != 0) { - return result; - } - result = compareStrings(evidence, alert2.evidence); if (result != 0) { return result; @@ -530,6 +540,10 @@ public boolean equals(Object obj) { } Alert item = (Alert) obj; + if (this.alertId == item.getAlertId()) { + return true; + } + if (risk != item.risk) { return false; } @@ -548,7 +562,11 @@ public boolean equals(Object obj) { if (!method.equalsIgnoreCase(item.method)) { return false; } - if (!uri.equalsIgnoreCase(item.uri)) { + if (nodeName != null && item.nodeName != null) { + if (!nodeName.equals(item.nodeName)) { + return false; + } + } else if (!uri.equalsIgnoreCase(item.uri)) { return false; } if (!param.equalsIgnoreCase(item.param)) { @@ -591,7 +609,11 @@ public int hashCode() { result = prime * result + pluginId; result = prime * result + alertRef.hashCode(); result = prime * result + method.hashCode(); - result = prime * result + uri.hashCode(); + if (nodeName != null) { + result = prime * result + nodeName.hashCode(); + } else { + result = prime * result + uri.hashCode(); + } result = prime * result + ((attack == null) ? 0 : attack.hashCode()); return result; } @@ -603,6 +625,7 @@ public int hashCode() { */ public Alert newInstance() { Alert item = new Alert(this.pluginId); + item.setAlertId(alertId); item.setHistoryId(historyId); item.setRiskConfidence(this.risk, this.confidence); item.setName(this.name); @@ -621,6 +644,7 @@ public Alert newInstance() { item.setWascId(this.wascId); item.setSource(this.source); item.setTags(this.tags); + item.setNodeName(this.nodeName); return item; } @@ -1048,6 +1072,24 @@ public void setAlertRef(String alertRef) { this.alertRef = alertRef; } + /** + * Get the node name. + * + * @since 2.17.0 + */ + public String getNodeName() { + return nodeName; + } + + /** + * Set the node name + * + * @since 2.17.0 + */ + public void setNodeName(String nodeName) { + this.nodeName = nodeName; + } + /** * Returns a new alert builder. * diff --git a/zap/src/main/java/org/parosproxy/paros/db/RecordAlert.java b/zap/src/main/java/org/parosproxy/paros/db/RecordAlert.java index 748428a3978..356acb6d851 100644 --- a/zap/src/main/java/org/parosproxy/paros/db/RecordAlert.java +++ b/zap/src/main/java/org/parosproxy/paros/db/RecordAlert.java @@ -29,6 +29,7 @@ // ZAP: 2020/11/03 Add alertRef field. // ZAP: 2021/04/30 Add input vector to Alert // ZAP: 2022/02/03 Removed deprecated getReliability() and setReliability() +// ZAP: 2025/10/01 Added support for nodeName. package org.parosproxy.paros.db; public class RecordAlert { @@ -56,6 +57,7 @@ public class RecordAlert { private int sourceHistoryId = 0; private int sourceId = 0; private String alertRef = ""; + private String nodeName; public RecordAlert() {} @@ -80,7 +82,8 @@ public RecordAlert( int sourceHistoryId, int sourceId, String alertRef, - String inputVector) { + String inputVector, + String nodeName) { setAlertId(alertId); setScanId(scanId); setPluginId(pluginId); @@ -102,6 +105,7 @@ public RecordAlert( setWascId(wascId); setSourceId(sourceId); setAlertRef(alertRef); + setNodeName(nodeName); } /** @@ -381,4 +385,22 @@ public String getAlertRef() { public void setAlertRef(String alertRef) { this.alertRef = alertRef; } + + /** + * Gets the node name, which is a normalised version of the URL. + * + * @since 2.17.0 + */ + public String getNodeName() { + return nodeName; + } + + /** + * Sets the node name, which is a normalised version of the URL. + * + * @since 2.17.0 + */ + public void setNodeName(String nodeName) { + this.nodeName = nodeName; + } } diff --git a/zap/src/main/java/org/parosproxy/paros/db/TableAlert.java b/zap/src/main/java/org/parosproxy/paros/db/TableAlert.java index 3ce2415ca2f..34c719b7080 100644 --- a/zap/src/main/java/org/parosproxy/paros/db/TableAlert.java +++ b/zap/src/main/java/org/parosproxy/paros/db/TableAlert.java @@ -52,7 +52,8 @@ RecordAlert write( int sourceHistoryId, int sourceId, String alertRef, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException; Vector getAlertListBySession(long sessionId) throws DatabaseException; @@ -77,7 +78,8 @@ void update( int cweId, int wascId, int sourceHistoryId, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException; void updateHistoryIds(int alertId, int historyId, int sourceHistoryId) throws DatabaseException; diff --git a/zap/src/main/java/org/parosproxy/paros/db/paros/ParosTableAlert.java b/zap/src/main/java/org/parosproxy/paros/db/paros/ParosTableAlert.java index a068b614713..9f382ce54f0 100644 --- a/zap/src/main/java/org/parosproxy/paros/db/paros/ParosTableAlert.java +++ b/zap/src/main/java/org/parosproxy/paros/db/paros/ParosTableAlert.java @@ -39,6 +39,7 @@ // ZAP: 2019/06/05 Normalise format/style. // ZAP: 2021/04/30 Add input vector to Alert // ZAP: 2021/08/24 Remove the "(non-Javadoc)" comments. +// ZAP: 2025/10/01 Added support for nodeName. package org.parosproxy.paros.db.paros; import java.sql.CallableStatement; @@ -82,6 +83,7 @@ public class ParosTableAlert extends ParosAbstractTable implements TableAlert { private static final String SOURCEHISTORYID = "SOURCEHISTORYID"; private static final String SOURCEID = "SOURCEID"; private static final String ALERTREF = "ALERTREF"; + private static final String NODENAME = "NODENAME"; private PreparedStatement psRead = null; private PreparedStatement psInsert = null; @@ -152,7 +154,9 @@ protected void reconnect(Connection conn) throws DatabaseException { + ALERTREF + "," + INPUT_VECTOR - + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + "," + + NODENAME + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); psGetIdLastInsert = conn.prepareCall("CALL IDENTITY();"); psDeleteAlert = conn.prepareStatement( @@ -195,6 +199,8 @@ protected void reconnect(Connection conn) throws DatabaseException { + SOURCEHISTORYID + " = ?, " + INPUT_VECTOR + + " = ?, " + + NODENAME + " = ? " + "WHERE " + ALERTID @@ -307,6 +313,15 @@ private void updateTable(Connection connection) throws DatabaseException { + ALERTREF + " VARCHAR(256) DEFAULT ''"); } + if (!DbUtils.hasColumn(connection, TABLE_NAME, NODENAME)) { + DbUtils.execute( + connection, + "ALTER TABLE " + + TABLE_NAME + + " ADD COLUMN " + + NODENAME + + " VARCHAR(1048576) DEFAULT NULL"); + } } catch (SQLException e) { throw new DatabaseException(e); } @@ -346,7 +361,8 @@ public synchronized RecordAlert write( int sourceHistoryId, int sourceId, String alertRef, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException { try { @@ -370,6 +386,7 @@ public synchronized RecordAlert write( psInsert.setInt(18, sourceId); psInsert.setString(19, alertRef); psInsert.setString(20, inputVector); + psInsert.setString(21, nodeName); psInsert.executeUpdate(); int id; @@ -409,7 +426,8 @@ private RecordAlert build(ResultSet rs) throws DatabaseException { rs.getInt(SOURCEHISTORYID), rs.getInt(SOURCEID), rs.getString(ALERTREF), - rs.getString(INPUT_VECTOR)); + rs.getString(INPUT_VECTOR), + rs.getString(NODENAME)); } return alert; } catch (SQLException e) { @@ -505,7 +523,8 @@ public synchronized void update( int cweId, int wascId, int sourceHistoryId, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException { try { @@ -524,7 +543,8 @@ public synchronized void update( psUpdate.setInt(13, wascId); psUpdate.setInt(14, sourceHistoryId); psUpdate.setString(15, inputVector); - psUpdate.setInt(16, alertId); + psUpdate.setString(16, nodeName); + psUpdate.setInt(17, alertId); psUpdate.executeUpdate(); } catch (SQLException e) { throw new DatabaseException(e); diff --git a/zap/src/main/java/org/parosproxy/paros/model/HistoryReference.java b/zap/src/main/java/org/parosproxy/paros/model/HistoryReference.java index ae66ca8121f..42039b62486 100644 --- a/zap/src/main/java/org/parosproxy/paros/model/HistoryReference.java +++ b/zap/src/main/java/org/parosproxy/paros/model/HistoryReference.java @@ -67,6 +67,7 @@ // ZAP: 2022/06/27 Add TYPE_PARAM_DIGGER. // ZAP: 2022/09/21 Use format specifiers instead of concatenation when logging. // ZAP: 2023/01/10 Tidy up logger. +// ZAP: 2025/10/01 Alert handling tweaks. package org.parosproxy.paros.model; import java.util.ArrayList; @@ -670,38 +671,37 @@ public synchronized boolean addAlert(Alert alert) { alert.setHistoryRef(this); added = true; } - // Try to add to the SiteHNode anyway - that will also check if its already added + // Try to add to the SiteNode anyway - that will also check if its already added if (this.siteNode != null) { siteNode.addAlert(alert); } return added; } - public synchronized void updateAlert(Alert alert) { - // If there are no alerts yet + private Alert getAlert(int alertId) { if (alerts == null) { - return; + return null; } + return alerts.stream().filter(a -> a.getAlertId() == alertId).findFirst().orElse(null); + } - for (Alert a : alerts) { - if (a.getAlertId() == alert.getAlertId()) { - // Have to use the alertId instead of 'equals' as any of the - // other params might have changed - this.alerts.remove(a); - this.alerts.add(alert); - if (this.siteNode != null) { - siteNode.updateAlert(alert); - } - return; + public synchronized void updateAlert(Alert alert) { + Alert a = getAlert(alert.getAlertId()); + if (a != null) { + this.alerts.remove(a); + this.alerts.add(alert); + if (this.siteNode != null) { + siteNode.updateAlert(alert); } } } public synchronized void deleteAlert(Alert alert) { - if (alerts != null) { - alerts.remove(alert); + Alert a = getAlert(alert.getAlertId()); + if (a != null) { + alerts.remove(a); if (siteNode != null) { - siteNode.deleteAlert(alert); + siteNode.deleteAlert(a); } } } @@ -722,10 +722,7 @@ public synchronized void deleteAllAlerts() { * @see #addAlert(Alert) */ public synchronized boolean hasAlert(Alert alert) { - if (alerts == null) { - return false; - } - return alerts.contains(alert); + return this.getAlert(alert.getAlertId()) != null; } /** diff --git a/zap/src/main/java/org/parosproxy/paros/model/SiteNode.java b/zap/src/main/java/org/parosproxy/paros/model/SiteNode.java index e7c931b2fad..386eb303177 100644 --- a/zap/src/main/java/org/parosproxy/paros/model/SiteNode.java +++ b/zap/src/main/java/org/parosproxy/paros/model/SiteNode.java @@ -65,16 +65,14 @@ // ZAP: 2023/01/10 Tidy up logger. // ZAP: 2024/01/19 Accept cleanName via constructor and cache non-regex hierarchic node name. // ZAP: 2024/02/23 Correct name of hosts without children. +// ZAP: 2025/10/01 Use AlertSet for alerts. package org.parosproxy.paros.model; import java.awt.EventQueue; import java.net.URL; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Set; import java.util.Vector; import javax.swing.ImageIcon; import javax.swing.tree.DefaultMutableTreeNode; @@ -85,6 +83,7 @@ import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.view.View; import org.zaproxy.zap.control.ExtensionFactory; +import org.zaproxy.zap.extension.alert.AlertSet; import org.zaproxy.zap.model.SessionStructure; @SuppressWarnings("serial") @@ -100,7 +99,7 @@ public class SiteNode extends DefaultMutableTreeNode { private Vector pastHistoryList = new Vector<>(10); // ZAP: Support for linking Alerts to SiteNodes private SiteMap siteMap = null; - private Set alerts = Collections.synchronizedSet(new HashSet<>()); + private AlertSet alerts = new AlertSet(); private boolean justSpidered = false; // private boolean justAJAXSpidered = false; private ArrayList icons = null; @@ -214,34 +213,7 @@ public List getCustomIcons() { * @see #isHighestAlert(Alert) */ private void calculateHighestAlert() { - synchronized (alerts) { - highestAlert = null; - for (Alert alert : alerts) { - if (isHighestAlert(alert)) { - highestAlert = alert; - } - } - calculateHighestAlert = false; - } - } - - /** - * Tells whether or not the given alert is the alert with highest risk than the current highest - * alert. - * - *

{@link Alert#CONFIDENCE_FALSE_POSITIVE False positive alerts} are ignored. - * - * @param alert the alert to check - * @return {@code true} if it's the alert with highest risk, {@code false} otherwise. - */ - private boolean isHighestAlert(Alert alert) { - if (alert.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { - return false; - } - if (highestAlert == null) { - return true; - } - return alert.getRisk() > highestAlert.getRisk(); + highestAlert = alerts.getHighestRisk(); } public Alert getHighestAlert() { @@ -425,25 +397,23 @@ public Vector getPastHistoryReference() { } public boolean hasAlert(Alert alert) { - if (alert == null) { - throw new IllegalArgumentException("Alert must not be null"); - } - return alerts.contains(alert); + return alerts.hasAlert(alert.getAlertId()); } public void addAlert(Alert alert) { if (alert == null) { throw new IllegalArgumentException("Alert must not be null"); } - if (!this.alerts.add(alert)) { + if (this.getParent() == null) { + // This is the top "Sites" node, no need to record the alerts here return; } - if (isHighestAlert(alert)) { - highestAlert = alert; - } - if (this.getParent() != null) { - this.getParent().addAlert(alert); + if (this.alerts.hasSimilar(alert)) { + return; } + this.alerts.add(alert); + this.getParent().addAlert(alert); + highestAlert = alerts.getHighestRisk(); if (this.siteMap != null) { // Adding alert might affect the nodes visibility in a filtered tree siteMap.applyFilter(this); @@ -456,19 +426,10 @@ public void updateAlert(Alert alert) { throw new IllegalArgumentException("Alert must not be null"); } boolean updated = false; - synchronized (alerts) { - for (Iterator it = alerts.iterator(); it.hasNext(); ) { - if (it.next().getAlertId() == alert.getAlertId()) { - it.remove(); - updated = true; - this.alerts.add(alert); - setCalculateHighestAlertIfSameAlert(alert); - break; - } - } - } + updated = alerts.add(alert); if (updated) { + setCalculateHighestAlertIfSameAlert(alert); if (this.getParent() != null) { this.getParent().updateAlert(alert); } @@ -488,9 +449,7 @@ public void updateAlert(Alert alert) { * @return a new {@code List} containing the {@code Alert}s */ public List getAlerts() { - synchronized (alerts) { - return new ArrayList<>(alerts); - } + return alerts.getAllUnique(); } private void clearChildAlert(Alert alert, SiteNode child) { @@ -593,7 +552,7 @@ private void clearChildAlerts(List alerts) { if (this.getChildCount() > 0) { SiteNode c = (SiteNode) this.getFirstChild(); while (c != null) { - alertsToRemove.removeAll(c.alerts); + alertsToRemove.removeAll(c.alerts.getAll()); c = (SiteNode) this.getChildAfter(c); } } diff --git a/zap/src/main/java/org/zaproxy/zap/db/sql/SqlTableAlert.java b/zap/src/main/java/org/zaproxy/zap/db/sql/SqlTableAlert.java index c47573cb928..4e0614d13e9 100644 --- a/zap/src/main/java/org/zaproxy/zap/db/sql/SqlTableAlert.java +++ b/zap/src/main/java/org/zaproxy/zap/db/sql/SqlTableAlert.java @@ -56,6 +56,7 @@ public class SqlTableAlert extends SqlAbstractTable implements TableAlert { private static final String SOURCEHISTORYID = DbSQL.getSQL("alert.field.sourcehistoryid"); private static final String SOURCEID = DbSQL.getSQL("alert.field.sourceid"); private static final String ALERTREF = DbSQL.getSQL("alert.field.alertref"); + private static final String NODENAME = DbSQL.getSQL("alert.field.nodename"); public SqlTableAlert() {} @@ -105,6 +106,10 @@ private void updateTable(Connection connection) throws DatabaseException { if (!DbUtils.hasColumn(connection, TABLE_NAME, ALERTREF)) { DbUtils.execute(connection, DbSQL.getSQL("alert.ps.addalertref")); } + + if (!DbUtils.hasColumn(connection, TABLE_NAME, NODENAME)) { + DbUtils.execute(connection, DbSQL.getSQL("alert.ps.addnodename")); + } } catch (SQLException e) { throw new DatabaseException(e); } @@ -147,7 +152,8 @@ public synchronized RecordAlert write( int sourceHistoryId, int sourceId, String alertRef, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException { SqlPreparedStatementWrapper psInsert = null; @@ -173,6 +179,7 @@ public synchronized RecordAlert write( psInsert.getPs().setInt(18, sourceId); psInsert.getPs().setString(19, alertRef); psInsert.getPs().setString(20, inputVector); + psInsert.getPs().setString(21, nodeName); psInsert.getPs().executeUpdate(); @@ -215,7 +222,8 @@ private RecordAlert build(ResultSet rs) throws DatabaseException { rs.getInt(SOURCEHISTORYID), rs.getInt(SOURCEID), rs.getString(ALERTREF), - rs.getString(INPUT_VECTOR)); + rs.getString(INPUT_VECTOR), + rs.getString(NODENAME)); } return alert; } catch (SQLException e) { @@ -267,7 +275,8 @@ public synchronized void update( int cweId, int wascId, int sourceHistoryId, - String inputVector) + String inputVector, + String nodeName) throws DatabaseException { SqlPreparedStatementWrapper psUpdate = null; @@ -289,6 +298,7 @@ public synchronized void update( psUpdate.getPs().setInt(14, sourceHistoryId); psUpdate.getPs().setString(15, inputVector); psUpdate.getPs().setInt(16, alertId); + psUpdate.getPs().setString(17, nodeName); psUpdate.getPs().executeUpdate(); } catch (SQLException e) { throw new DatabaseException(e); diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertAPI.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertAPI.java index 12de6855502..6210eeaef29 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertAPI.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertAPI.java @@ -285,11 +285,11 @@ public JSON toJSON() { } AlertTreeModel model = extension.getTreeModel(); - AlertNode root = (AlertNode) model.getRoot(); + AlertNode root = model.getRoot(); Enumeration enumAllAlerts = root.children(); while (enumAllAlerts.hasMoreElements()) { AlertNode child = (AlertNode) enumAllAlerts.nextElement(); - Alert alert = child.getUserObject(); + Alert alert = child.getAlert(); ApiResponseList alertList = filterAlertInstances(child, url, recurse); if (!alertList.getItems().isEmpty()) { @@ -306,11 +306,11 @@ public JSON toJSON() { int falsePositiveCount = 0; AlertTreeModel model = extension.getTreeModel(); - AlertNode root = (AlertNode) model.getRoot(); + AlertNode root = model.getRoot(); Enumeration enumAllAlerts = root.children(); while (enumAllAlerts.hasMoreElements()) { AlertNode child = (AlertNode) enumAllAlerts.nextElement(); - Alert alert = child.getUserObject(); + Alert alert = child.getAlert(); ApiResponseList alertList = filterAlertInstances(child, url, recurse); if (!alertList.getItems().isEmpty()) { @@ -444,12 +444,12 @@ public ApiResponse handleApiAction(String name, JSONObject params) throws ApiExc private static ApiResponseList filterAlertInstances( AlertNode alertNode, String url, boolean recurse) { - ApiResponseList alertList = new ApiResponseList(alertNode.getUserObject().getName()); + ApiResponseList alertList = new ApiResponseList(alertNode.getAlert().getName()); Enumeration enumAlertInsts = alertNode.children(); while (enumAlertInsts.hasMoreElements()) { AlertNode childAlert = (AlertNode) enumAlertInsts.nextElement(); if (!url.isEmpty()) { - String alertUrl = childAlert.getUserObject().getUri(); + String alertUrl = childAlert.getAlert().getUri(); if (!alertUrl.startsWith(url)) { continue; } @@ -463,7 +463,7 @@ private static ApiResponseList filterAlertInstances( } } } - alertList.addItem(alertSummaryToSet(childAlert.getUserObject())); + alertList.addItem(alertSummaryToSet(childAlert.getAlert())); } return alertList; } @@ -525,6 +525,7 @@ private static ApiResponseSet alertToSet(Alert alert) { alert.getName()); // Deprecated in 2.5.0, maintain for compatibility with custom // code map.put("name", alert.getName()); + map.put("nodeName", alert.getNodeName()); map.put("description", alert.getDescription()); map.put("risk", Alert.MSG_RISK[alert.getRisk()]); map.put("confidence", Alert.MSG_CONFIDENCE[alert.getConfidence()]); diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertNode.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertNode.java index 6499d64188a..274b30f2cd1 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertNode.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertNode.java @@ -46,16 +46,40 @@ public AlertNode(int risk, String nodeName, Comparator childComparato this.childComparator = new AlertNodeComparatorWrapper(childComparator); } + /** Sets an alert for this node. The {@link #setAlert(Alert)} method should be used instead. */ @Override public void setUserObject(Object userObject) { - if (!(userObject instanceof Alert)) { + if (userObject instanceof Alert alert) { + this.setAlert(alert); + } else { throw new IllegalArgumentException("Parameter userObject must be an Alert."); } - this.alert = (Alert) userObject; } + /** + * Set the alert associated with this node. + * + * @since 2.17.0 + */ + public void setAlert(Alert alert) { + this.alert = alert; + } + + /** + * Returns an alert associated with this node. The {@link #getAlert()} method should be used + * instead. This method will be change to throw an exception in a future release. + */ @Override public Alert getUserObject() { + return this.getAlert(); + } + + /** + * Gets the alert associated with this node. + * + * @since 2.17.0 + */ + public Alert getAlert() { return alert; } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertPanel.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertPanel.java index c1aef4aff51..4a866b63a56 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertPanel.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertPanel.java @@ -50,7 +50,6 @@ import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.apache.logging.log4j.LogManager; @@ -340,7 +339,7 @@ public void setLinkWithSitesTreeSelection(boolean enabled) { sitesTree.addTreeSelectionListener(getLinkWithSitesTreeSelectionListener()); } else { extension.setMainTreeModel(); - ((AlertNode) getLinkWithSitesTreeModel().getRoot()).removeAllChildren(); + getLinkWithSitesTreeModel().getRoot().removeAllChildren(); view.getSiteTreePanel() .getTreeSite() .removeTreeSelectionListener(getLinkWithSitesTreeSelectionListener()); @@ -390,7 +389,7 @@ private void recreateLinkWithSitesTreeModel(SiteNode siteNode) { if (siteNode == null) { throw new IllegalArgumentException("Parameter siteNode must not be null."); } - ((AlertNode) getLinkWithSitesTreeModel().getRoot()).removeAllChildren(); + getLinkWithSitesTreeModel().getRoot().removeAllChildren(); if (siteNode.isRoot()) { getLinkWithSitesTreeModel().reload(); extension.recalcAlerts(); @@ -479,10 +478,9 @@ public void show(Component invoker, int x, int y) { SortedSet historyReferenceIdsAdded = new TreeSet<>(); for (TreePath path : treeAlert.getSelectionPaths()) { final AlertNode node = (AlertNode) path.getLastPathComponent(); - final Object userObject = node.getUserObject(); - if (userObject instanceof Alert) { - HistoryReference historyReference = - ((Alert) userObject).getHistoryRef(); + final Alert alert = node.getAlert(); + if (alert != null) { + HistoryReference historyReference = alert.getHistoryRef(); if (historyReference != null && !historyReferenceIdsAdded.contains( historyReference.getHistoryId())) { @@ -519,19 +517,12 @@ public void mouseClicked(java.awt.event.MouseEvent e) { new javax.swing.event.TreeSelectionListener() { @Override public void valueChanged(javax.swing.event.TreeSelectionEvent e) { - DefaultMutableTreeNode node = - (DefaultMutableTreeNode) - treeAlert.getLastSelectedPathComponent(); - if (node != null && node.getUserObject() != null) { - Object obj = node.getUserObject(); - if (obj instanceof Alert) { - Alert alert = (Alert) obj; - setMessage(alert.getMessage(), alert.getEvidence()); - treeAlert.requestFocusInWindow(); - getAlertViewPanel().displayAlert(alert); - } else { - getAlertViewPanel().clearAlert(); - } + AlertNode node = (AlertNode) treeAlert.getLastSelectedPathComponent(); + if (node != null && node.getAlert() != null) { + Alert alert = node.getAlert(); + setMessage(alert.getMessage(), alert.getEvidence()); + treeAlert.requestFocusInWindow(); + getAlertViewPanel().displayAlert(alert); } else { getAlertViewPanel().clearAlert(); } @@ -596,21 +587,20 @@ private Set getSelectedAlertsImpl(boolean allAlerts) { Set alerts = new HashSet<>(); if (!allAlerts) { - DefaultMutableTreeNode alertNode = - (DefaultMutableTreeNode) paths[0].getLastPathComponent(); - alerts.add((Alert) alertNode.getUserObject()); + AlertNode alertNode = (AlertNode) paths[0].getLastPathComponent(); + alerts.add(alertNode.getAlert()); return alerts; } for (TreePath path : paths) { - DefaultMutableTreeNode alertNode = (DefaultMutableTreeNode) path.getLastPathComponent(); + AlertNode alertNode = (AlertNode) path.getLastPathComponent(); if (alertNode.getChildCount() == 0) { - alerts.add((Alert) alertNode.getUserObject()); + alerts.add(alertNode.getAlert()); continue; } for (int j = 0; j < alertNode.getChildCount(); j++) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) alertNode.getChildAt(j); - alerts.add((Alert) node.getUserObject()); + AlertNode node = alertNode.getChildAt(j); + alerts.add(node.getAlert()); } } return alerts; @@ -762,13 +752,10 @@ public void valueChanged(TreeSelectionEvent e) { } private void editSelectedAlert() { - DefaultMutableTreeNode node = - (DefaultMutableTreeNode) treeAlert.getLastSelectedPathComponent(); - if (node != null && node.getUserObject() != null) { - Object obj = node.getUserObject(); - if (obj instanceof Alert) { - Alert alert = (Alert) obj; - + AlertNode node = (AlertNode) treeAlert.getLastSelectedPathComponent(); + if (node != null) { + Alert alert = node.getAlert(); + if (alert != null) { extension.showAlertEditDialog(alert); } } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertSet.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertSet.java new file mode 100644 index 00000000000..a96442a1636 --- /dev/null +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertSet.java @@ -0,0 +1,200 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.alert; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import org.parosproxy.paros.core.scanner.Alert; + +/** + * A collection of Alerts with unique alertIds. + * + * @since 2.17.0 + */ +public class AlertSet { + + private Map map = new ConcurrentHashMap<>(); + private Alert highestRisk; + + public AlertSet() {} + + /** + * Add the specified alert to the set. + * + * @param alert the alert + * @return true if the alert was not already in the set. + */ + public synchronized boolean add(Alert alert) { + boolean added = map.put(alert.getAlertId(), alert) == null; + highestRisk = getHighestRisk(highestRisk, alert); + return added; + } + + /** + * Adds all of the specified alerts to the set. + * + * @param alerts the alerts + * @return true if one or more of the alerts were not already in the set. + */ + public boolean addAll(List alerts) { + AtomicBoolean changed = new AtomicBoolean(false); + alerts.forEach( + a -> { + if (add(a)) { + changed.set(true); + } + }); + return changed.get(); + } + + /** + * Removes the specified alert using the alertId. + * + * @param alert the alert to remove + * @return true if the alert was removed + */ + public synchronized boolean remove(Alert alert) { + Alert a = map.remove(alert.getAlertId()); + if (a != null) { + if (highestRisk != null && a.getAlertId() == highestRisk.getAlertId()) { + calculateHighestRisk(); + } + return true; + } + return false; + } + + /** + * Removes all of the specified alerts. + * + * @param alerts the alerts to remove + * @return true if one or more of the alerts were removed. + */ + public boolean removeAll(List alerts) { + AtomicBoolean changed = new AtomicBoolean(false); + alerts.forEach( + a -> { + if (remove(a)) { + changed.set(true); + } + }); + return changed.get(); + } + + /** Returns true if the set contains an alert with the given id. */ + public boolean hasAlert(int alertId) { + return map.containsKey(alertId); + } + + /** + * Returns true if the set contains a similar alert, based on the alert properties but using the + * alert nodeName instead of the URI. + */ + public boolean hasSimilar(Alert alert) { + return map.values().stream().anyMatch(a -> a.compareTo(alert) == 0); + } + + /** Returns one of the alerts. */ + public Alert get() { + return map.values().stream().findAny().orElse(null); + } + + /** Returns all of the alerts. */ + public List getAll() { + return map.values().stream().toList(); + } + + /** Returns all of the unique alerts. */ + public List getAllUnique() { + return new ArrayList<>(new TreeSet<>(map.values())); + } + + /** Clears the set. */ + public void clear() { + map.clear(); + } + + /** + * Returns true if the set is empty. + * + * @return + */ + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * Returns the alert with the highest risk, or null if there are no non false positive alerts. + */ + public Alert getHighestRisk() { + return highestRisk; + } + + private void calculateHighestRisk() { + highestRisk = null; + for (Alert a : map.values()) { + highestRisk = getHighestRisk(highestRisk, a); + } + } + + /** + * Returns the number of alerts in the set. + * + * @return + */ + public int size() { + return map.size(); + } + + /** + * Returns the alert with the highest risk, or null if the alerts are null or false positives. + * + * @param a1 the first alert, may be null + * @param a2 the second alert, must not be null + * @return the alert with the highest risk, or null + */ + private static Alert getHighestRisk(Alert a1, Alert a2) { + if (a2.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { + if (a1 != null && a1.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { + return null; + } + return a1; + } + if (a1 == null) { + return a2; + } + return a2.getRisk() > a1.getRisk() ? a2 : a1; + } + + @Override + public String toString() { + return "AlertSet[" + + map.values().stream() + .map(Alert::getAlertId) + .map(Object::toString) + .collect(Collectors.joining(",")) + + "]"; + } +} diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeCellRenderer.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeCellRenderer.java index e076440c8fb..de6cba670f1 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeCellRenderer.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeCellRenderer.java @@ -23,7 +23,6 @@ import javax.swing.ImageIcon; import javax.swing.JTree; import javax.swing.tree.DefaultTreeCellRenderer; -import org.parosproxy.paros.core.scanner.Alert; import org.zaproxy.zap.utils.DisplayUtils; import org.zaproxy.zap.view.SiteMapTreeCellRenderer; @@ -73,9 +72,7 @@ public Component getTreeCellRendererComponent( this.setIcon(FOLDER_CLOSED_ICON); } } else if (alertNode.getParent().isRoot()) { - // Add the alert flag icon - Alert alert = alertNode.getUserObject(); - this.setIcon(alert.getIcon()); + this.setIcon(alertNode.getAlert().getIcon()); } else { this.setIcon(LEAF_ICON); } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeModel.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeModel.java index b0f7fb3e3f8..6d4313ed7b5 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeModel.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeModel.java @@ -22,12 +22,14 @@ import java.awt.EventQueue; import java.util.Comparator; import javax.swing.tree.DefaultTreeModel; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.view.View; +@SuppressWarnings("serial") class AlertTreeModel extends DefaultTreeModel { private static final long serialVersionUID = 1L; @@ -65,15 +67,27 @@ public void run() { } } - private synchronized void addPathEventHandler(Alert alert) { - AlertNode parent = (AlertNode) getRoot(); - parent = findAndAddChild(parent, alert.getName(), alert); + /** + * @since 2.17.0 + */ + @Override + public AlertNode getRoot() { + return (AlertNode) super.getRoot(); + } + + protected synchronized AlertNode addPathEventHandler(Alert alert) { + AlertNode parent = findAndAddGroup(getRoot(), alert.getName(), alert); // Show the method first, if present String method = ""; if (alert.getMethod() != null) { - method = alert.getMethod() + ": "; + method = alert.getMethod() + ":"; } - addLeaf(parent, method + alert.getUri(), alert); + String name = + method + + (StringUtils.isNotEmpty(alert.getNodeName()) + ? alert.getNodeName() + : alert.getUri()); + return addLeaf(parent, name, alert); } private AlertNode findLeafNodeForAlert(AlertNode parent, Alert alert) { @@ -81,8 +95,7 @@ private AlertNode findLeafNodeForAlert(AlertNode parent, Alert alert) { AlertNode child = parent.getChildAt(i); if (child.getChildCount() == 0) { // Its a leaf node - if (child.getUserObject() != null - && child.getUserObject().getAlertId() == alert.getAlertId()) { + if (child.getAlert() != null && child.getAlert().compareTo(alert) == 0) { return child; } } else { @@ -97,15 +110,15 @@ private AlertNode findLeafNodeForAlert(AlertNode parent, Alert alert) { } public AlertNode getAlertNode(Alert alert) { - AlertNode parent = (AlertNode) getRoot(); + AlertNode parent = getRoot(); int risk = alert.getRisk(); if (alert.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { // Special case! risk = -1; } - AlertNode needle = new AlertNode(risk, alert.getName()); - needle.setUserObject(alert); + AlertNode needle = new AlertNode(risk, alert.getName(), GROUP_ALERT_CHILD_COMPARATOR); + needle.setAlert(alert); int idx = parent.findIndex(needle); if (idx < 0) { return null; @@ -118,16 +131,16 @@ public AlertNode getAlertNode(Alert alert) { return parent.getChildAt(idx); } - void updatePath(final Alert originalAlert, final Alert alert) { + void updatePath(final Alert alert) { if (!View.isInitialised() || EventQueue.isDispatchThread()) { - updatePathEventHandler(originalAlert, alert); + updatePathEventHandler(alert); } else { try { EventQueue.invokeLater( new Runnable() { @Override public void run() { - updatePathEventHandler(originalAlert, alert); + updatePathEventHandler(alert); } }); } catch (Exception e) { @@ -136,39 +149,53 @@ public void run() { } } - private synchronized void updatePathEventHandler(Alert originalAlert, Alert alert) { + private synchronized void updatePathEventHandler(Alert alert) { - AlertNode node = findLeafNodeForAlert((AlertNode) getRoot(), alert); + AlertNode node = findLeafNodeForAlert(getRoot(), alert); if (node != null) { // Remove the old version AlertNode parent = node.getParent(); - this.removeNodeFromParent(node); + // Cannot use removeNodeFromParent as the risk or name might have changed + removeChildById(parent, alert.getAlertId()); nodeStructureChanged(parent); if (parent.getChildCount() == 0) { // Parent has no other children, remove it also this.removeNodeFromParent(parent); - nodeStructureChanged((AlertNode) this.getRoot()); + nodeStructureChanged(this.getRoot()); } } // Add it back in again this.addPath(alert); } - private AlertNode findAndAddChild(AlertNode parent, String nodeName, Alert alert) { + private void removeChildById(AlertNode parent, int alertId) { + int idx = -1; + for (int i = 0; i < parent.getChildCount(); i++) { + if (parent.getChildAt(i).getAlert().getAlertId() == alertId) { + idx = i; + break; + } + } + if (idx >= 0) { + parent.remove(idx); + } + } + + private AlertNode findAndAddGroup(AlertNode parent, String nodeName, Alert alert) { int risk = alert.getRisk(); if (alert.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { // Special case! risk = -1; } - int idx = parent.findIndex(new AlertNode(risk, nodeName)); + AlertNode node = new AlertNode(risk, nodeName, ALERT_CHILD_COMPARATOR); + int idx = parent.findIndex(node); if (idx < 0) { idx = -(idx + 1); - AlertNode node = new AlertNode(risk, nodeName, ALERT_CHILD_COMPARATOR); - node.setUserObject(alert); + node.setAlert(alert); parent.insert(node, idx); nodesWereInserted(parent, new int[] {idx}); nodeChanged(parent); @@ -177,27 +204,29 @@ private AlertNode findAndAddChild(AlertNode parent, String nodeName, Alert alert return parent.getChildAt(idx); } - private void addLeaf(AlertNode parent, String nodeName, Alert alert) { + private AlertNode addLeaf(AlertNode parent, String nodeName, Alert alert) { int risk = alert.getRisk(); if (alert.getConfidence() == Alert.CONFIDENCE_FALSE_POSITIVE) { // Special case! risk = -1; } - AlertNode needle = new AlertNode(risk, nodeName); - needle.setUserObject(alert); + AlertNode needle = new AlertNode(risk, nodeName, ALERT_CHILD_COMPARATOR); + needle.setAlert(alert); int idx = parent.findIndex(needle); if (idx < 0) { idx = -(idx + 1); parent.insert(needle, idx); nodesWereInserted(parent, new int[] {idx}); nodeChanged(parent); + return needle; } + return null; } public synchronized void deletePath(Alert alert) { - AlertNode node = findLeafNodeForAlert((AlertNode) getRoot(), alert); + AlertNode node = findLeafNodeForAlert(getRoot(), alert); if (node != null) { AlertNode parent = node.getParent(); if (parent.getChildCount() == 1) { @@ -211,8 +240,8 @@ public synchronized void deletePath(Alert alert) { // Remove it this.removeNodeFromParent(node); - if (parent.getUserObject() == node.getUserObject()) { - parent.setUserObject(parent.getChildAt(0).getUserObject()); + if (parent.getAlert() == node.getAlert()) { + parent.setAlert(parent.getChildAt(0).getAlert()); } this.nodeChanged(parent); } @@ -236,7 +265,7 @@ private static class AlertChildNodeComparator implements Comparator { @Override public int compare(AlertNode alertNode, AlertNode anotherAlertNode) { - return alertNode.getUserObject().compareTo(anotherAlertNode.getUserObject()); + return alertNode.getAlert().compareTo(anotherAlertNode.getAlert()); } } } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertViewPanel.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertViewPanel.java index 580da36cbed..6a6464a9b73 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertViewPanel.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertViewPanel.java @@ -755,25 +755,22 @@ public void setParamNames(String[] paramNames) { } public Alert getAlert() { - if (!editable && originalAlert != null) { - Alert alert = originalAlert.newInstance(); - alert.setAlertId(originalAlert.getAlertId()); - alert.setName((String) alertEditName.getSelectedItem()); - alert.setParam((String) alertEditParam.getSelectedItem()); - alert.setRiskConfidence( + if (originalAlert != null) { + originalAlert.setName((String) alertEditName.getSelectedItem()); + originalAlert.setParam((String) alertEditParam.getSelectedItem()); + originalAlert.setRiskConfidence( alertEditRisk.getSelectedIndex(), alertEditConfidence.getSelectedIndex()); - alert.setDescription(alertDescription.getText()); - alert.setOtherInfo(alertOtherInfo.getText()); - alert.setSolution(alertSolution.getText()); - alert.setReference(alertReference.getText()); - alert.setEvidence(alertEvidence.getText()); - alert.setInputVector(originalAlert.getInputVector()); - alert.setCweId(alertEditCweId.getValue()); - alert.setWascId(alertEditWascId.getValue()); - alert.setHistoryRef(historyRef); - alert.setTags(getAlertTags()); + originalAlert.setDescription(alertDescription.getText()); + originalAlert.setOtherInfo(alertOtherInfo.getText()); + originalAlert.setSolution(alertSolution.getText()); + originalAlert.setReference(alertReference.getText()); + originalAlert.setEvidence(alertEditEvidence.getText()); + originalAlert.setCweId(alertEditCweId.getValue()); + originalAlert.setWascId(alertEditWascId.getValue()); + originalAlert.setHistoryRef(historyRef); + originalAlert.setTags(getAlertTags()); - return alert; + return originalAlert; } Alert alert = @@ -822,6 +819,9 @@ public Alert getAlert() { msg); alert.setHistoryId(historyId); alert.setTags(getAlertTags()); + if (originalAlert != null) { + alert.setNodeName(originalAlert.getNodeName()); + } return alert; } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/ExtensionAlert.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/ExtensionAlert.java index e55316ca81e..ee35052bfe1 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/ExtensionAlert.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/ExtensionAlert.java @@ -35,6 +35,7 @@ import java.util.Vector; import javax.swing.JTree; import javax.swing.tree.TreePath; +import org.apache.commons.httpclient.URIException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -68,6 +69,7 @@ import org.zaproxy.zap.extension.help.ExtensionHelp; import org.zaproxy.zap.model.SessionStructure; import org.zaproxy.zap.model.Target; +import org.zaproxy.zap.utils.ThreadUtils; import org.zaproxy.zap.view.popup.MenuWeights; public class ExtensionAlert extends ExtensionAdaptor @@ -180,6 +182,15 @@ public void alertFound(Alert alert, HistoryReference ref) { return; } + if (alert.getMessage().getHistoryRef() != null) { + ref = alert.getMessage().getHistoryRef(); + } + try { + alert.setNodeName(SessionStructure.getNodeName(getModel(), alert.getMessage())); + } catch (URIException e) { + LOGGER.error(e.getMessage(), e); + } + try { int sourceHistoryId = alert.getSourceHistoryId(); LOGGER.debug("alertFound {} {}", alert.getName(), alert.getUri()); @@ -216,30 +227,37 @@ public void alertFound(Alert alert, HistoryReference ref) { this.applyOverrides(alert); writeAlertToDB(alert, ref); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } - try { - if (getView() == null || EventQueue.isDispatchThread()) { - SessionStructure.addPath(Model.getSingleton(), ref, alert.getMessage()); + final HistoryReference fRef = ref; + ThreadUtils.invokeAndWaitHandled(() -> alertFoundEventHandler(alert, fRef)); + } + + private void alertFoundEventHandler(Alert alert, HistoryReference ref) { + try { + synchronized (this.getTreeModel()) { + if (this.getTreeModel().addPathEventHandler(alert) != null) { + if (isInFilter(alert)) { + this.getFilteredTreeModel().addPath(alert); + } + if (getView() != null) { + getAlertPanel().expandRoot(); + this.recalcAlerts(); + } } else { - final HistoryReference fRef = ref; - final HttpMessage fMsg = alert.getMessage(); - EventQueue.invokeAndWait( - new Runnable() { - - @Override - public void run() { - SessionStructure.addPath(Model.getSingleton(), fRef, fMsg); - } - }); + return; } + } + try { + SessionStructure.addPath(Model.getSingleton(), ref, alert.getMessage()); ref.addAlert(alert); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } - addAlertToTree(alert); - // Clear the message so that it can be GC'ed alert.setMessage(null); @@ -496,6 +514,14 @@ private AlertTreeModel getFilteredTreeModel() { return filteredTreeModel; } + /** + * Returns a YAML representation of the Alert tree. Only for use in testing / debugging! It + * should not be relied upon and may be removed in a future version without being deprecated. + */ + public String getTextAlertTree() { + return TextAlertTree.toString(getTreeModel()); + } + private void writeAlertToDB(Alert alert, HistoryReference ref) throws HttpMalformedHeaderException, DatabaseException { @@ -525,7 +551,8 @@ private void writeAlertToDB(Alert alert, HistoryReference ref) alert.getSourceHistoryId(), alert.getSource().getId(), alert.getAlertRef(), - alert.getInputVector()); + alert.getInputVector(), + alert.getNodeName()); int alertId = recordAlert.getAlertId(); alert.setAlertId(alertId); @@ -544,7 +571,7 @@ public void updateAlert(Alert alert) throws HttpMalformedHeaderException, Databa updateAlertInDB(alert); hRef.updateAlert(alert); publishAlertEvent(alert, AlertEventPublisher.ALERT_CHANGED_EVENT); - updateAlertInTree(alert, alert); + updateAlertInTree(alert); } } @@ -568,7 +595,8 @@ private void updateAlertInDB(Alert alert) alert.getCweId(), alert.getWascId(), alert.getSourceHistoryId(), - alert.getInputVector()); + alert.getInputVector(), + alert.getNodeName()); int alertId = alert.getAlertId(); TableAlertTag tableAlertTag = getModel().getDb().getTableAlertTag(); @@ -589,20 +617,31 @@ public void displayAlert(Alert alert) { this.alertPanel.getAlertViewPanel().displayAlert(alert); } + /** + * @deprecated (2.17.0) Use {@link #updateAlertInTree(Alert)} instead. + */ + @Deprecated public void updateAlertInTree(Alert originalAlert, Alert alert) { + this.updateAlertInTree(alert); + } + + /** + * @since 2.17.0 + */ + public void updateAlertInTree(Alert alert) { if (Constant.isLowMemoryOptionSet()) { return; } if (getView() == null || EventQueue.isDispatchThread()) { - updateAlertInTreeEventHandler(originalAlert, alert); + updateAlertInTreeEventHandler(alert); } else { try { EventQueue.invokeAndWait( new Runnable() { @Override public void run() { - updateAlertInTreeEventHandler(originalAlert, alert); + updateAlertInTreeEventHandler(alert); } }); } catch (Exception e) { @@ -611,18 +650,21 @@ public void run() { } } - private void updateAlertInTreeEventHandler(Alert originalAlert, Alert alert) { - this.getTreeModel().updatePath(originalAlert, alert); + private void updateAlertInTreeEventHandler(Alert alert) { + this.getTreeModel().updatePath(alert); if (isInFilter(alert)) { - this.getFilteredTreeModel().updatePath(originalAlert, alert); + this.getFilteredTreeModel().updatePath(alert); } this.recalcAlerts(); if (hasView()) { JTree alertTree = this.getAlertPanel().getTreeAlert(); - TreePath alertPath = new TreePath(getTreeModel().getAlertNode(alert).getPath()); - alertTree.setSelectionPath(alertPath); - alertTree.scrollPathToVisible(alertPath); + AlertNode node = getTreeModel().getAlertNode(alert); + if (node != null) { + TreePath alertPath = new TreePath(node.getPath()); + alertTree.setSelectionPath(alertPath); + alertTree.scrollPathToVisible(alertPath); + } } } @@ -1041,8 +1083,8 @@ public String getDescription() { public void sessionScopeChanged(Session session) { // Have to recheck all alerts to see if they are in scope synchronized (this.getTreeModel()) { - ((AlertNode) this.getFilteredTreeModel().getRoot()).removeAllChildren(); - AlertNode root = (AlertNode) this.getTreeModel().getRoot(); + this.getFilteredTreeModel().getRoot().removeAllChildren(); + AlertNode root = this.getTreeModel().getRoot(); filterTree(root); this.getFilteredTreeModel().nodeStructureChanged(root); } @@ -1051,8 +1093,8 @@ public void sessionScopeChanged(Session session) { } private void filterTree(AlertNode node) { - if (node.getUserObject() != null) { - Alert alert = node.getUserObject(); + Alert alert = node.getAlert(); + if (alert != null) { if (this.isInFilter(alert)) { this.getFilteredTreeModel().addPath(alert); } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/TextAlertTree.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/TextAlertTree.java new file mode 100644 index 00000000000..6277b7d25b9 --- /dev/null +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/TextAlertTree.java @@ -0,0 +1,64 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.alert; + +import org.parosproxy.paros.core.scanner.Alert; + +public class TextAlertTree { + + private TextAlertTree() {} + + /** + * Returns a YAML representation of the Alert tree. Only for use in testing / debugging! + * + * @param model The Alert tree model + * @return a YAML representation of the Alert tree + * @since 2.17.0 + */ + public static String toString(AlertTreeModel model) { + StringBuilder sb = new StringBuilder(); + + dumpRoot(model.getRoot(), sb); + + return sb.toString(); + } + + private static void dumpRoot(AlertNode root, StringBuilder sb) { + sb.append("- Alerts\n"); + root.children().asIterator().forEachRemaining(child -> dumpAlert((AlertNode) child, sb)); + } + + private static void dumpAlert(AlertNode node, StringBuilder sb) { + sb.append(" - "); + sb.append(Alert.MSG_RISK[node.getRisk()]); + sb.append(": "); + sb.append(node.getNodeName()); + sb.append("\n"); + node.children() + .asIterator() + .forEachRemaining(child -> dumpAlertInstance((AlertNode) child, sb)); + } + + private static void dumpAlertInstance(AlertNode node, StringBuilder sb) { + sb.append(" - "); + sb.append(node.getNodeName()); + sb.append("\n"); + } +} diff --git a/zap/src/main/resources/org/zaproxy/zap/resources/zapdb.script b/zap/src/main/resources/org/zaproxy/zap/resources/zapdb.script index 36d9165df91..b4ff009fb12 100644 --- a/zap/src/main/resources/org/zaproxy/zap/resources/zapdb.script +++ b/zap/src/main/resources/org/zaproxy/zap/resources/zapdb.script @@ -40,7 +40,7 @@ CREATE INDEX HISTORY_INDEX ON PUBLIC.HISTORY(URI,METHOD,REQBODY,SESSIONID,HISTTY CREATE INDEX INDEX_HISTORY_HISTTYPE ON PUBLIC.HISTORY(HISTTYPE) CREATE INDEX INDEX_HISTORY_SESSIONID ON PUBLIC.HISTORY(SESSIONID) CREATE CACHED TABLE PUBLIC.SESSION(SESSIONID BIGINT NOT NULL PRIMARY KEY,SESSIONNAME VARCHAR(32768) DEFAULT '',LASTACCESS TIMESTAMP DEFAULT LOCALTIMESTAMP NOT NULL) -CREATE CACHED TABLE PUBLIC.ALERT(ALERTID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL PRIMARY KEY,SCANID INTEGER NOT NULL,PLUGINID INTEGER DEFAULT 0,ALERT VARCHAR(16777216) DEFAULT '',RISK INTEGER DEFAULT 0,RELIABILITY INTEGER DEFAULT 1,DESCRIPTION VARCHAR(16777216) DEFAULT '',URI VARCHAR(1048576) DEFAULT '',PARAM VARCHAR(32768) DEFAULT '',OTHERINFO VARCHAR(16777216) DEFAULT '',SOLUTION VARCHAR(16777216) DEFAULT '',REFERENCE VARCHAR(16777216) DEFAULT '',HISTORYID INTEGER, SOURCEHISTORYID INTEGER DEFAULT 0, ATTACK VARCHAR(32768) DEFAULT '', EVIDENCE VARCHAR(16777216) DEFAULT '', CWEID INTEGER DEFAULT -1, WASCID INTEGER DEFAULT -1, SOURCEID INTEGER DEFAULT 0, INPUT_VECTOR VARCHAR(256) DEFAULT '') +CREATE CACHED TABLE PUBLIC.ALERT(ALERTID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL PRIMARY KEY,SCANID INTEGER NOT NULL,PLUGINID INTEGER DEFAULT 0,ALERT VARCHAR(16777216) DEFAULT '',RISK INTEGER DEFAULT 0,RELIABILITY INTEGER DEFAULT 1,DESCRIPTION VARCHAR(16777216) DEFAULT '',URI VARCHAR(1048576) DEFAULT '',PARAM VARCHAR(32768) DEFAULT '',OTHERINFO VARCHAR(16777216) DEFAULT '',SOLUTION VARCHAR(16777216) DEFAULT '',REFERENCE VARCHAR(16777216) DEFAULT '',HISTORYID INTEGER, SOURCEHISTORYID INTEGER DEFAULT 0, ATTACK VARCHAR(32768) DEFAULT '', EVIDENCE VARCHAR(16777216) DEFAULT '', CWEID INTEGER DEFAULT -1, WASCID INTEGER DEFAULT -1, SOURCEID INTEGER DEFAULT 0, INPUT_VECTOR VARCHAR(256) DEFAULT '', NODENAME VARCHAR(1048576) DEFAULT NULL) CREATE INDEX ALERT_INDEX ON PUBLIC.ALERT(SOURCEHISTORYID) CREATE INDEX INDEX_ALERT_SOURCEID ON PUBLIC.ALERT(SOURCEID) ALTER TABLE PUBLIC.ALERT ALTER COLUMN ALERTID RESTART WITH 0 diff --git a/zap/src/test/java/org/parosproxy/paros/core/scanner/AlertUnitTest.java b/zap/src/test/java/org/parosproxy/paros/core/scanner/AlertUnitTest.java index efb3c5693ac..97dfefa7610 100644 --- a/zap/src/test/java/org/parosproxy/paros/core/scanner/AlertUnitTest.java +++ b/zap/src/test/java/org/parosproxy/paros/core/scanner/AlertUnitTest.java @@ -141,6 +141,8 @@ void shouldNotEqualIfAlertRefDifferent() { Alert alertA = new Alert(1); Alert alertB = new Alert(1); // When + alertA.setAlertId(0); + alertB.setAlertId(1); alertA.setAlertRef("1-1"); alertB.setAlertRef("1-2"); boolean equals = alertA.equals(alertB); @@ -154,6 +156,8 @@ void shouldNotCompareIfAlertRefDifferent() { Alert alertA = new Alert(1); Alert alertB = new Alert(1); // When + alertA.setAlertId(0); + alertB.setAlertId(1); alertA.setAlertRef("1-1"); alertB.setAlertRef("1-2"); int cmp = alertA.compareTo(alertB); diff --git a/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertSetUnitTest.java b/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertSetUnitTest.java new file mode 100644 index 00000000000..b56c2a057fe --- /dev/null +++ b/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertSetUnitTest.java @@ -0,0 +1,283 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.alert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.parosproxy.paros.core.scanner.Alert; + +public class AlertSetUnitTest { + + private AlertSet alertSet; + + @BeforeEach + void setUp() throws Exception { + alertSet = new AlertSet(); + } + + @Test + void shouldAddUniqueAlerts() { + // Given + Alert a1 = getAlert(0, "Test Alert", "https://www.example.com/a/"); + Alert a2 = getAlert(1, "Test Alert", "https://www.example.com/b/"); + Alert a3 = getAlert(2, "Test Alert", "https://www.example.com/c/"); + Alert a4 = getAlert(3, "Test Alert", "https://www.example.com/d/"); + Alert a5 = getAlert(4, "Test Alert", "https://www.example.com/e/"); + List alerts = List.of(a3, a4, a5); + + // When + boolean add1 = alertSet.add(a1); + boolean add2 = alertSet.add(a2); + boolean add3 = alertSet.addAll(alerts); + + // Then + assertTrue(add1); + assertTrue(add2); + assertTrue(add3); + assertEquals(5, alertSet.size()); + assertTrue(alertSet.hasAlert(a1.getAlertId())); + assertTrue(alertSet.hasAlert(a2.getAlertId())); + assertTrue(alertSet.hasAlert(a3.getAlertId())); + assertTrue(alertSet.hasAlert(a4.getAlertId())); + assertTrue(alertSet.hasAlert(a5.getAlertId())); + } + + @Test + void shouldReplaceExistingAlerts() { + // Given + Alert a1 = getAlert(0, "Test Alert", "https://www.example.com/a/"); + Alert a2 = getAlert(1, "Test Alert", "https://www.example.com/b/"); + Alert a3 = getAlert(2, "Test Alert", "https://www.example.com/c/"); + Alert a4 = getAlert(3, "Test Alert", "https://www.example.com/d/"); + Alert a5 = getAlert(4, "Test Alert", "https://www.example.com/e/"); + List alerts = List.of(a3, a4, a5); + alertSet.addAll(List.of(a1, a2, a3, a4, a5)); + + // When + boolean add1 = alertSet.add(a1); + boolean add2 = alertSet.add(a2); + boolean add3 = alertSet.addAll(alerts); + + // Then + assertFalse(add1); + assertFalse(add2); + assertFalse(add3); + assertEquals(5, alertSet.size()); + assertTrue(alertSet.hasAlert(a1.getAlertId())); + assertTrue(alertSet.hasAlert(a2.getAlertId())); + assertTrue(alertSet.hasAlert(a3.getAlertId())); + assertTrue(alertSet.hasAlert(a4.getAlertId())); + assertTrue(alertSet.hasAlert(a5.getAlertId())); + } + + @Test + void shouldRemoveAlerts() { + // Given + Alert a1 = getAlert(0, "Test Alert", "https://www.example.com/a/"); + Alert a2 = getAlert(1, "Test Alert", "https://www.example.com/b/"); + Alert a3 = getAlert(2, "Test Alert", "https://www.example.com/c/"); + Alert a4 = getAlert(3, "Test Alert", "https://www.example.com/d/"); + Alert a5 = getAlert(4, "Test Alert", "https://www.example.com/e/"); + List alerts = List.of(a4, a5); + alertSet.addAll(List.of(a1, a2, a3, a4, a5)); + + // When + boolean rem1 = alertSet.remove(a1); + boolean rem2 = alertSet.removeAll(alerts); + boolean rem3 = alertSet.remove(a1); + boolean rem4 = alertSet.removeAll(alerts); + + // Then + assertTrue(rem1); + assertTrue(rem2); + assertFalse(rem3); + assertFalse(rem4); + assertEquals(2, alertSet.size()); + assertTrue(alertSet.hasAlert(a2.getAlertId())); + assertTrue(alertSet.hasAlert(a3.getAlertId())); + } + + @Test + void shouldGetAlerts() { + // Given + Alert a1 = getAlert(0, "Test Alert", "https://www.example.com/a/"); + Alert a2 = getAlert(1, "Test Alert", "https://www.example.com/b/"); + + // When + Alert get1 = alertSet.get(); + List getl1 = alertSet.getAll(); + alertSet.add(a1); + Alert get2 = alertSet.get(); + List getl2 = alertSet.getAll(); + alertSet.add(a2); + Alert get3 = alertSet.get(); + List getl3 = alertSet.getAll(); + + // Then + assertNull(get1); + assertEquals(0, getl1.size()); + assertEquals(a1, get2); + assertEquals(1, getl2.size()); + assertEquals(a1, getl2.get(0)); + assertEquals(a1, get2); + assertNotNull(get3); + assertEquals(2, getl3.size()); + assertTrue(getl3.contains(a1)); + assertTrue(getl3.contains(a2)); + } + + @Test + void shouldFindSimilarAlert() { + // Given + Alert a1 = + getAlert( + 0, + "Test Alert", + "https://www.example.com/(test)", + "https://www.example.com/?test=1"); + Alert a2 = + getAlert( + 1, + "Test Alert", + "https://www.example.com/(test)", + "https://www.example.com/?test=2"); + Alert a3 = + getAlert( + 2, + "Test Alert", + "https://www.example.com/(x)", + "https://www.example.com/?test=2"); + + // When + alertSet.add(a1); + + // Then + assertTrue(alertSet.hasSimilar(a2)); + assertFalse(alertSet.hasSimilar(a3)); + } + + @Test + void shouldClearAlerts() { + // Given + Alert a1 = getAlert(0, "Test Alert", "https://www.example.com/a/"); + Alert a2 = getAlert(1, "Test Alert", "https://www.example.com/b/"); + Alert a3 = getAlert(2, "Test Alert", "https://www.example.com/c/"); + Alert a4 = getAlert(3, "Test Alert", "https://www.example.com/d/"); + Alert a5 = getAlert(4, "Test Alert", "https://www.example.com/e/"); + alertSet.addAll(List.of(a1, a2, a3, a4, a5)); + + // When + boolean empty1 = alertSet.isEmpty(); + alertSet.clear(); + boolean empty2 = alertSet.isEmpty(); + + // Then + assertFalse(empty1); + assertTrue(empty2); + assertEquals(0, alertSet.size()); + } + + @Test + void shouldGetHighestRiskAlert() { + // Given + Alert a1 = + getAlert( + 0, + "Test Alert", + "https://www.example.com/a/", + Alert.RISK_HIGH, + Alert.CONFIDENCE_FALSE_POSITIVE); + Alert a2 = + getAlert( + 1, + "Test Alert", + "https://www.example.com/b/", + Alert.RISK_INFO, + Alert.CONFIDENCE_HIGH); + Alert a3 = + getAlert( + 2, + "Test Alert", + "https://www.example.com/c/", + Alert.RISK_LOW, + Alert.CONFIDENCE_MEDIUM); + Alert a4 = + getAlert( + 3, + "Test Alert", + "https://www.example.com/d/", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_HIGH); + Alert a5 = + getAlert( + 4, + "Test Alert", + "https://www.example.com/e/", + Alert.RISK_HIGH, + Alert.CONFIDENCE_USER_CONFIRMED); + + // When + Alert high0 = alertSet.getHighestRisk(); + alertSet.add(a1); + Alert high1 = alertSet.getHighestRisk(); + alertSet.add(a2); + Alert high2 = alertSet.getHighestRisk(); + alertSet.add(a3); + Alert high3 = alertSet.getHighestRisk(); + alertSet.add(a4); + Alert high4 = alertSet.getHighestRisk(); + alertSet.add(a5); + Alert high5 = alertSet.getHighestRisk(); + + // Then + assertNull(high0); + assertNull(high1); + assertEquals(a2, high2); + assertEquals(a3, high3); + assertEquals(a4, high4); + assertEquals(a5, high5); + } + + private Alert getAlert(int alertId, String name, String nodeName, String uri) { + Alert a1 = new Alert(0, Alert.RISK_INFO, Alert.CONFIDENCE_MEDIUM, name); + a1.setUri(uri); + a1.setNodeName(nodeName); + a1.setAlertId(alertId); + return a1; + } + + private Alert getAlert(int alertId, String name, String uri, int risk, int confidence) { + Alert a = this.getAlert(alertId, name, uri, uri); + a.setRisk(risk); + a.setConfidence(confidence); + return a; + } + + private Alert getAlert(int alertId, String name, String uri) { + return this.getAlert(alertId, name, uri, uri); + } +} diff --git a/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java b/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java new file mode 100644 index 00000000000..bc119bb27a1 --- /dev/null +++ b/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java @@ -0,0 +1,420 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2025 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.zap.extension.alert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.extension.history.ExtensionHistory; +import org.zaproxy.zap.WithConfigsTest; + +public class AlertTreeModelUnitTest extends WithConfigsTest { + + private AlertTreeModel atModel; + + @BeforeEach + void setUp() throws Exception { + WithConfigsTest.setUpConstantMessages(); + atModel = new AlertTreeModel(); + } + + @Test + void shouldAddUniqueAlerts() { + // Given + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com", + "https://www.example.com", + Alert.RISK_HIGH, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.net", + "https://www.example.net", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com", + "https://www.example.com", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a3); + atModel.addPath(a2); + + // Then + + assertEquals( + """ + - Alerts + - High: Alert A + - :https://www.example.com + - Medium: Alert A + - :https://www.example.com + - :https://www.example.net + """, + TextAlertTree.toString(atModel)); + + assertEquals(a1, atModel.getRoot().getChildAt(0).getChildAt(0).getAlert()); + assertEquals(a3, atModel.getRoot().getChildAt(1).getChildAt(0).getAlert()); + assertEquals(a2, atModel.getRoot().getChildAt(1).getChildAt(1).getAlert()); + } + + @Test + void shouldAddDuplicateAlerts() { + // Given + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=3", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + + // Then + assertEquals(1, atModel.getRoot().getChildCount()); + + // Only child - Medium risk + assertEquals("Alert A", atModel.getRoot().getChildAt(0).getNodeName()); + assertEquals(Alert.RISK_MEDIUM, atModel.getRoot().getChildAt(0).getRisk()); + assertEquals(1, atModel.getRoot().getChildAt(0).getChildCount()); + + assertEquals( + ":https://www.example.com(a)", + atModel.getRoot().getChildAt(0).getChildAt(0).getNodeName()); + assertEquals(Alert.RISK_MEDIUM, atModel.getRoot().getChildAt(0).getChildAt(0).getRisk()); + } + + @Test + void shouldFindDuplicateAlerts() { + // Given + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=3", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + + AlertNode an1 = atModel.getAlertNode(a1); + AlertNode an2 = atModel.getAlertNode(a2); + AlertNode an3 = atModel.getAlertNode(a3); + + // Then + assertEquals(":https://www.example.com(a)", an1.getNodeName()); + assertEquals(":https://www.example.com(a)", an2.getNodeName()); + assertEquals(":https://www.example.com(a)", an3.getNodeName()); + } + + @Test + void shouldChangeDuplicateAlerts() { + ExtensionHistory extHistory = mock(ExtensionHistory.class); + given(extensionLoader.getExtension(ExtensionHistory.class)).willReturn(extHistory); + + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=3", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + a1.setRisk(Alert.RISK_HIGH); + atModel.updatePath(a1); + + // Then + assertEquals(1, atModel.getRoot().getChildCount()); + + // Only child - Medium risk + assertEquals("Alert A", atModel.getRoot().getChildAt(0).getNodeName()); + assertEquals(Alert.RISK_HIGH, atModel.getRoot().getChildAt(0).getRisk()); + assertEquals(1, atModel.getRoot().getChildAt(0).getChildCount()); + + assertEquals( + ":https://www.example.com(a)", + atModel.getRoot().getChildAt(0).getChildAt(0).getNodeName()); + assertEquals(Alert.RISK_HIGH, atModel.getRoot().getChildAt(0).getChildAt(0).getRisk()); + } + + @Test + void shouldDeleteUniqueAlert() { + // Given + ExtensionHistory extHistory = mock(ExtensionHistory.class); + given(extensionLoader.getExtension(ExtensionHistory.class)).willReturn(extHistory); + + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com/a1", + "https://www.example.com/a1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com/a2", + "https://www.example.com/a2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.net", + "https://www.example.net", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + + atModel.deletePath(a1); + + // Then + assertEquals(1, atModel.getRoot().getChildCount()); + + assertEquals( + """ + - Alerts + - Medium: Alert A + - :https://www.example.com/a2 + - :https://www.example.net + """, + TextAlertTree.toString(atModel)); + + assertEquals(a2, atModel.getRoot().getChildAt(0).getChildAt(0).getAlert()); + assertEquals(a3, atModel.getRoot().getChildAt(0).getChildAt(1).getAlert()); + } + + @Test + void shouldChangeUniqueAlert() { + // Given + ExtensionHistory extHistory = mock(ExtensionHistory.class); + given(extensionLoader.getExtension(ExtensionHistory.class)).willReturn(extHistory); + + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com/a1", + "https://www.example.com/a1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.net", + "https://www.example.net", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com/a2", + "https://www.example.com/a2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + + a1.setRisk(Alert.RISK_HIGH); + atModel.updatePath(a1); + + // Then + assertEquals( + """ + - Alerts + - High: Alert A + - :https://www.example.com/a1 + - Medium: Alert A + - :https://www.example.com/a2 + - :https://www.example.net + """, + TextAlertTree.toString(atModel)); + + assertEquals(a1, atModel.getRoot().getChildAt(0).getChildAt(0).getAlert()); + assertEquals(a3, atModel.getRoot().getChildAt(1).getChildAt(0).getAlert()); + assertEquals(a2, atModel.getRoot().getChildAt(1).getChildAt(1).getAlert()); + } + + @Test + void shouldDeleteNodeWhenNoAlertsLeft() { + // Given + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=1", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=2", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=3", + Alert.RISK_MEDIUM, + Alert.CONFIDENCE_MEDIUM); + + // When + atModel.addPath(a1); + atModel.addPath(a2); + atModel.addPath(a3); + + atModel.deletePath(a1); + atModel.deletePath(a3); + atModel.deletePath(a2); + + // Then + assertEquals(0, atModel.getRoot().getChildCount()); + } + + private static Alert newAlert( + int pluginId, + int id, + String name, + String nodeName, + String uri, + int risk, + int confidence) { + Alert alert = new Alert(pluginId, risk, confidence, name); + alert.setUri(uri); + alert.setAlertId(id); + alert.setNodeName(nodeName); + return alert; + } +} diff --git a/zap/src/test/java/org/zaproxy/zap/extension/alert/ExtensionAlertUnitTest.java b/zap/src/test/java/org/zaproxy/zap/extension/alert/ExtensionAlertUnitTest.java index ba74f091bc8..a31522ed0c4 100644 --- a/zap/src/test/java/org/zaproxy/zap/extension/alert/ExtensionAlertUnitTest.java +++ b/zap/src/test/java/org/zaproxy/zap/extension/alert/ExtensionAlertUnitTest.java @@ -22,6 +22,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @@ -29,8 +31,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Stream; +import org.apache.commons.httpclient.URI; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -38,9 +42,15 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedStatic; +import org.parosproxy.paros.Constant; import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.model.HistoryReference; +import org.parosproxy.paros.model.Model; +import org.parosproxy.paros.model.Session; import org.parosproxy.paros.network.HttpMessage; +import org.zaproxy.zap.model.ParameterParser; +import org.zaproxy.zap.model.StandardParameterParser; +import org.zaproxy.zap.utils.I18N; class ExtensionAlertUnitTest { @@ -537,12 +547,27 @@ void shouldReplaceOnlySpecifiedTag() { } @Test - void shouldCopyCorrectHistoryTags() { + void shouldCopyCorrectHistoryTags() throws Exception { // Given + HistoryReference href = mock(HistoryReference.class); + when(href.getHistoryType()).thenReturn(1); + when(href.getHistoryId()).thenReturn(1); + + Constant.messages = new I18N(Locale.ENGLISH); + Session session = mock(Session.class); + + Model model = mock(Model.class); + Model.setSingletonForTesting(model); + given(model.getSession()).willReturn(session); + ParameterParser pp = new StandardParameterParser(); + given(session.getUrlParamParser(anyString())).willReturn(pp); + extAlert.initModel(model); + Alert alert = newAlert(1); alert.setUri("https://www.example.com"); alert.setSourceHistoryId(1); HttpMessage msg = new HttpMessage(); + msg.getRequestHeader().setURI(new URI("https://www.example.com", true)); alert.setMessage(msg); try (MockedStatic hr = mockStatic(HistoryReference.class)) { @@ -560,10 +585,6 @@ void shouldCopyCorrectHistoryTags() { "ALERT-TAG:III"); hr.when(() -> HistoryReference.getTags(1)).thenReturn(tags); - HistoryReference href = mock(HistoryReference.class); - when(href.getHistoryType()).thenReturn(1); - when(href.getHistoryId()).thenReturn(1); - // When extAlert.alertFound(alert, href); Map alertTags = alert.getTags(); From 9e6db7e4b8080d951a11877098248943bd7dc76c Mon Sep 17 00:00:00 2001 From: thc202 Date: Thu, 16 Oct 2025 14:36:46 +0100 Subject: [PATCH 3/3] Disable HTTP auth itest They are currently failing because of automation protections accessing the target. Signed-off-by: thc202 --- .../{jigsaw-basic-user.yaml => jigsaw-basic-user.yaml.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker/integration_tests/configs/plans/{jigsaw-basic-user.yaml => jigsaw-basic-user.yaml.disabled} (100%) diff --git a/docker/integration_tests/configs/plans/jigsaw-basic-user.yaml b/docker/integration_tests/configs/plans/jigsaw-basic-user.yaml.disabled similarity index 100% rename from docker/integration_tests/configs/plans/jigsaw-basic-user.yaml rename to docker/integration_tests/configs/plans/jigsaw-basic-user.yaml.disabled