From 9e8f3f85be6b0e361d920cfa16c66458b707e0a7 Mon Sep 17 00:00:00 2001 From: Simon Bennetts Date: Thu, 16 Oct 2025 17:50:21 +0100 Subject: [PATCH] Systemic alert support Signed-off-by: Simon Bennetts --- .../parosproxy/paros/core/scanner/Alert.java | 16 ++++ .../zap/extension/alert/AlertNode.java | 12 ++- .../zap/extension/alert/AlertPanel.java | 2 +- .../zap/extension/alert/AlertParam.java | 21 +++++ .../alert/AlertTreeCellRenderer.java | 15 +++- .../zap/extension/alert/AlertTreeModel.java | 13 +++- .../zap/extension/alert/ExtensionAlert.java | 41 +++++++++- .../extension/alert/OptionsAlertPanel.java | 29 +++++-- .../zaproxy/zap/resources/Messages.properties | 3 + .../alert/AlertTreeModelUnitTest.java | 42 ++++++---- .../alert/ExtensionAlertUnitTest.java | 78 +++++++++++++++++++ 11 files changed, 242 insertions(+), 30 deletions(-) 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 6fdc287d210..d0fd1914e6a 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 @@ -69,6 +69,7 @@ // 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. +// ZAP: 2025/10/16 Added support for systemic alerts. package org.parosproxy.paros.core.scanner; import java.net.URL; @@ -199,6 +200,8 @@ public static Source getSource(int id) { private static final String CWE_KEY = "CWE-"; private static final String CWE_URL_BASE = "https://cwe.mitre.org/data/definitions/"; + private static final String SYSTEMIC_KEY = "SYSTEMIC"; + private int alertId = -1; // ZAP: Changed default alertId private int historyId; private int pluginId = -1; @@ -231,6 +234,7 @@ public static Source getSource(int id) { private String alertRef = ""; private String nodeName; private Map tags = Collections.emptyMap(); + private Boolean systemic; public Alert(int pluginId) { this.pluginId = pluginId; @@ -1090,6 +1094,18 @@ public void setNodeName(String nodeName) { this.nodeName = nodeName; } + /** + * Returns true if the alert has the "SYSTEMIC" tag + * + * @since 2.17.0 + */ + public boolean isSystemic() { + if (systemic == null) { + systemic = this.getTags().containsKey(SYSTEMIC_KEY); + } + return systemic; + } + /** * Returns a new alert builder. * 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 274b30f2cd1..8a92679a9bf 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 @@ -34,6 +34,7 @@ public class AlertNode extends DefaultMutableTreeNode { private String nodeName = null; private int risk = -1; private Alert alert; + private boolean systemic; public AlertNode(int risk, String nodeName) { this(risk, nodeName, null); @@ -143,9 +144,6 @@ public int findIndex(AlertNode aChild) { @Override public String toString() { - if (this.getChildCount() > 1) { - return nodeName + " (" + this.getChildCount() + ")"; - } return nodeName; } @@ -161,6 +159,14 @@ public int getRisk() { return risk; } + public boolean isSystemic() { + return systemic; + } + + public void setSystemic(boolean systemic) { + this.systemic = systemic; + } + private static class AlertNodeComparatorWrapper implements Comparator { private final Comparator comparator; 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 4a866b63a56..3adc402e482 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 @@ -359,7 +359,7 @@ public void setLinkWithSitesTreeSelection(boolean enabled) { */ private AlertTreeModel getLinkWithSitesTreeModel() { if (linkWithSitesTreeModel == null) { - linkWithSitesTreeModel = new AlertTreeModel(); + linkWithSitesTreeModel = new AlertTreeModel(this.extension); } return linkWithSitesTreeModel; } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertParam.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertParam.java index d5b9c57b02d..42514f4c9d9 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertParam.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/AlertParam.java @@ -37,8 +37,12 @@ public class AlertParam extends AbstractParam { private static final String PARAM_OVERRIDES_FILENAME = PARAM_BASE_KEY + ".overridesFilename"; + private static final String PARAM_SYSTEMIC_LIMIT = PARAM_BASE_KEY + ".systemicLimit"; + private static final int DEFAULT_MAXIMUM_INSTANCES = 20; + private static final int DEFAULT_SYSTEMIC_LIMIT = 0; + /** * The number of maximum instances of each vulnerability included in a report. * @@ -46,6 +50,8 @@ public class AlertParam extends AbstractParam { */ private int maximumInstances = DEFAULT_MAXIMUM_INSTANCES; + private int systemicLimit = DEFAULT_SYSTEMIC_LIMIT; + private boolean mergeRelatedIssues = true; private String overridesFilename; @@ -58,6 +64,7 @@ public class AlertParam extends AbstractParam { @Override protected void parse() { maximumInstances = getInt(PARAM_MAXIMUM_INSTANCES, DEFAULT_MAXIMUM_INSTANCES); + systemicLimit = getInt(PARAM_SYSTEMIC_LIMIT, DEFAULT_SYSTEMIC_LIMIT); mergeRelatedIssues = getBoolean(PARAM_MERGE_RELATED_ISSUES, true); overridesFilename = getString(PARAM_OVERRIDES_FILENAME, ""); } @@ -86,6 +93,20 @@ public int getMaximumInstances() { return maximumInstances; } + public int getSystemicLimit() { + return systemicLimit; + } + + public void setSystemicLimit(int systemicLimit) { + int newValue = Math.max(0, systemicLimit); + + if (this.systemicLimit != newValue) { + this.systemicLimit = newValue; + + getConfig().setProperty(PARAM_SYSTEMIC_LIMIT, this.systemicLimit); + } + } + public boolean isMergeRelatedIssues() { return mergeRelatedIssues; } 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 de6cba670f1..0fb601cbca0 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,6 +23,7 @@ import javax.swing.ImageIcon; import javax.swing.JTree; import javax.swing.tree.DefaultTreeCellRenderer; +import org.parosproxy.paros.Constant; import org.zaproxy.zap.utils.DisplayUtils; import org.zaproxy.zap.view.SiteMapTreeCellRenderer; @@ -63,8 +64,7 @@ public Component getTreeCellRendererComponent( super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - if (value instanceof AlertNode) { - AlertNode alertNode = (AlertNode) value; + if (value instanceof AlertNode alertNode) { if (alertNode.isRoot()) { if (expanded) { this.setIcon(FOLDER_OPEN_ICON); @@ -76,6 +76,17 @@ public Component getTreeCellRendererComponent( } else { this.setIcon(LEAF_ICON); } + + if (alertNode.getChildCount() > 1) { + if (alertNode.isSystemic()) { + this.setText( + Constant.messages.getString("alert.label.namesystemic", alertNode)); + } else { + this.setText( + Constant.messages.getString( + "alert.label.namecount", alertNode, alertNode.getChildCount())); + } + } } return this; } 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 6d4313ed7b5..ce466261169 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 @@ -41,12 +41,15 @@ class AlertTreeModel extends DefaultTreeModel { private static final Logger LOGGER = LogManager.getLogger(AlertTreeModel.class); - AlertTreeModel() { + private ExtensionAlert ext; + + AlertTreeModel(ExtensionAlert ext) { super( new AlertNode( -1, Constant.messages.getString("alerts.tree.title"), GROUP_ALERT_CHILD_COMPARATOR)); + this.ext = ext; } void addPath(final Alert alert) { @@ -215,6 +218,14 @@ private AlertNode addLeaf(AlertNode parent, String nodeName, Alert alert) { needle.setAlert(alert); int idx = parent.findIndex(needle); if (idx < 0) { + // Not a duplicate alert + if (ext.isOverSystemicLimit(alert)) { + if (!parent.isSystemic()) { + parent.setSystemic(true); + nodeChanged(parent); + } + return null; + } idx = -(idx + 1); parent.insert(needle, idx); nodesWereInserted(parent, new int[] {idx}); 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 ee35052bfe1..3dbdecf19a5 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 @@ -33,6 +33,8 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JTree; import javax.swing.tree.TreePath; import org.apache.commons.httpclient.URIException; @@ -94,6 +96,9 @@ public class ExtensionAlert extends ExtensionAdaptor private Properties alertOverrides = new Properties(); private AlertAddDialog dialogAlertAdd; + private Map> siteToSystemicAlertMap = + new ConcurrentHashMap<>(); + public ExtensionAlert() { super(NAME); this.setOrder(27); @@ -170,7 +175,7 @@ private OptionsAlertPanel getOptionsPanel() { return optionsPanel; } - private AlertParam getAlertParam() { + protected AlertParam getAlertParam() { if (alertParam == null) { alertParam = new AlertParam(); } @@ -502,14 +507,14 @@ AlertPanel getAlertPanel() { AlertTreeModel getTreeModel() { if (treeModel == null) { - treeModel = new AlertTreeModel(); + treeModel = new AlertTreeModel(this); } return treeModel; } private AlertTreeModel getFilteredTreeModel() { if (filteredTreeModel == null) { - filteredTreeModel = new AlertTreeModel(); + filteredTreeModel = new AlertTreeModel(this); } return filteredTreeModel; } @@ -689,11 +694,12 @@ public void run() { } private void sessionChangedEventHandler(Session session) { - setTreeModel(new AlertTreeModel()); + setTreeModel(new AlertTreeModel(this)); treeModel = null; filteredTreeModel = null; hrefs = new HashMap<>(); + siteToSystemicAlertMap = new ConcurrentHashMap<>(); if (session == null) { // Null session indicated we're shutting down @@ -1259,4 +1265,31 @@ public boolean supportsLowMemory() { public boolean isNewAlert(Alert alertToCheck) { return (getTreeModel().getAlertNode(alertToCheck) == null); } + + /** + * Returns true if the given alert is over the systemic limit. If the alert is systemic then it + * will increment the count of that type of alert. + * + * @since 2.17.0 + */ + public boolean isOverSystemicLimit(Alert alert) { + if (alert == null || !alert.isSystemic()) { + return false; + } + try { + // Always count locally, even if the systemicLimit is zero as that could be changed + Map m = + siteToSystemicAlertMap.computeIfAbsent( + SessionStructure.getHostName(alert.getMsgUri()), + a -> new ConcurrentHashMap<>()); + int count = + m.computeIfAbsent(alert.getAlertRef(), a -> new AtomicInteger()) + .incrementAndGet(); + int limit = getAlertParam().getSystemicLimit(); + return limit > 0 && count > limit; + } catch (URIException e) { + // Ignore + } + return false; + } } diff --git a/zap/src/main/java/org/zaproxy/zap/extension/alert/OptionsAlertPanel.java b/zap/src/main/java/org/zaproxy/zap/extension/alert/OptionsAlertPanel.java index 8a36b6eb707..ee47e9d47a5 100644 --- a/zap/src/main/java/org/zaproxy/zap/extension/alert/OptionsAlertPanel.java +++ b/zap/src/main/java/org/zaproxy/zap/extension/alert/OptionsAlertPanel.java @@ -61,6 +61,8 @@ public class OptionsAlertPanel extends AbstractParamPanel { */ private ZapNumberSpinner maxInstances; + private ZapNumberSpinner systemicLimit; + private JCheckBox mergeRelatedIssues; private JTextField overridesFilename; @@ -72,15 +74,23 @@ public OptionsAlertPanel() { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(new EmptyBorder(2, 2, 2, 2)); + int y = 0; panel.add( - getMergeRelatedIssues(), LayoutHelper.getGBC(0, 0, 2, 1.0, new Insets(2, 2, 2, 2))); + getMergeRelatedIssues(), + LayoutHelper.getGBC(0, y++, 2, 1.0, new Insets(2, 2, 2, 2))); JLabel maxInstancesLabel = new JLabel(Constant.messages.getString("alert.optionspanel.label.maxinstances")); maxInstancesLabel.setLabelFor(getMaxInstances()); - panel.add(maxInstancesLabel, LayoutHelper.getGBC(0, 1, 1, 1.0, new Insets(2, 2, 2, 2))); - panel.add(getMaxInstances(), LayoutHelper.getGBC(1, 1, 1, 1.0, new Insets(2, 2, 2, 2))); + panel.add(maxInstancesLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2))); + panel.add(getMaxInstances(), LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2))); + + JLabel systemicLimitLabel = + new JLabel(Constant.messages.getString("alert.optionspanel.label.systemiclimit")); + systemicLimitLabel.setLabelFor(getSystemicLimit()); + panel.add(systemicLimitLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2))); + panel.add(getSystemicLimit(), LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2))); JButton overridesButton = new JButton( @@ -94,8 +104,8 @@ public OptionsAlertPanel() { overridesPanel.add(getOverridesFilename()); overridesPanel.add(overridesButton); - panel.add(overridesLabel, LayoutHelper.getGBC(0, 2, 1, 1.0, new Insets(2, 2, 2, 2))); - panel.add(overridesPanel, LayoutHelper.getGBC(1, 2, 1, 1.0, new Insets(2, 2, 2, 2))); + panel.add(overridesLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2))); + panel.add(overridesPanel, LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2))); add(panel); } @@ -123,6 +133,13 @@ private ZapNumberSpinner getMaxInstances() { return maxInstances; } + private ZapNumberSpinner getSystemicLimit() { + if (systemicLimit == null) { + systemicLimit = new ZapNumberSpinner(); + } + return systemicLimit; + } + private JTextField getOverridesFilename() { if (overridesFilename == null) { overridesFilename = new JTextField(20); @@ -136,6 +153,7 @@ public void initParam(Object obj) { final AlertParam param = options.getParamSet(AlertParam.class); getMaxInstances().setValue(Integer.valueOf(param.getMaximumInstances())); + getSystemicLimit().setValue(param.getSystemicLimit()); getMergeRelatedIssues().setSelected(param.isMergeRelatedIssues()); getMaxInstances().setEditable(param.isMergeRelatedIssues()); getOverridesFilename().setText(param.getOverridesFilename()); @@ -160,6 +178,7 @@ public void saveParam(Object obj) throws Exception { final AlertParam param = options.getParamSet(AlertParam.class); param.setMaximumInstances(getMaxInstances().getValue()); + param.setSystemicLimit(getSystemicLimit().getValue()); param.setMergeRelatedIssues(getMergeRelatedIssues().isSelected()); param.setOverridesFilename(getOverridesFilename().getText()); } diff --git a/zap/src/main/resources/org/zaproxy/zap/resources/Messages.properties b/zap/src/main/resources/org/zaproxy/zap/resources/Messages.properties index ff859943c27..a99df4dc8ee 100644 --- a/zap/src/main/resources/org/zaproxy/zap/resources/Messages.properties +++ b/zap/src/main/resources/org/zaproxy/zap/resources/Messages.properties @@ -91,6 +91,8 @@ alert.label.cweid = CWE ID: alert.label.desc = Description: alert.label.evidence = Evidence: alert.label.inputvector = Input Vector: +alert.label.namecount = {0} ({1}) +alert.label.namesystemic = {0} (Systemic) alert.label.other = Other Info: alert.label.parameter = Parameter: alert.label.ref = Reference: @@ -105,6 +107,7 @@ alert.optionspanel.button.overridesFilename = Select... alert.optionspanel.label.maxinstances = Max Alert Instances in Report: alert.optionspanel.label.mergerelated = Merge related alerts in report alert.optionspanel.label.overridesFilename = Alert Overrides File: +alert.optionspanel.label.systemiclimit = Systemic Limit: alert.optionspanel.name = Alerts alert.optionspanel.warn.badOverridesFilename = Invalid Alert Overrides File alert.source.active = Active 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 index bc119bb27a1..a7298cbe434 100644 --- a/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java +++ b/zap/src/test/java/org/zaproxy/zap/extension/alert/AlertTreeModelUnitTest.java @@ -23,20 +23,24 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import org.apache.commons.httpclient.URI; 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.parosproxy.paros.model.HistoryReference; import org.zaproxy.zap.WithConfigsTest; public class AlertTreeModelUnitTest extends WithConfigsTest { private AlertTreeModel atModel; + private ExtensionAlert extAlert; @BeforeEach void setUp() throws Exception { WithConfigsTest.setUpConstantMessages(); - atModel = new AlertTreeModel(); + extAlert = mock(ExtensionAlert.class); + atModel = new AlertTreeModel(extAlert); } @Test @@ -81,10 +85,10 @@ void shouldAddUniqueAlerts() { """ - Alerts - High: Alert A - - :https://www.example.com + - GET:https://www.example.com - Medium: Alert A - - :https://www.example.com - - :https://www.example.net + - GET:https://www.example.com + - GET:https://www.example.net """, TextAlertTree.toString(atModel)); @@ -138,7 +142,7 @@ void shouldAddDuplicateAlerts() { assertEquals(1, atModel.getRoot().getChildAt(0).getChildCount()); assertEquals( - ":https://www.example.com(a)", + "GET:https://www.example.com(a)", atModel.getRoot().getChildAt(0).getChildAt(0).getNodeName()); assertEquals(Alert.RISK_MEDIUM, atModel.getRoot().getChildAt(0).getChildAt(0).getRisk()); } @@ -184,9 +188,9 @@ void shouldFindDuplicateAlerts() { 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()); + assertEquals("GET:https://www.example.com(a)", an1.getNodeName()); + assertEquals("GET:https://www.example.com(a)", an2.getNodeName()); + assertEquals("GET:https://www.example.com(a)", an3.getNodeName()); } @Test @@ -238,7 +242,7 @@ void shouldChangeDuplicateAlerts() { assertEquals(1, atModel.getRoot().getChildAt(0).getChildCount()); assertEquals( - ":https://www.example.com(a)", + "GET:https://www.example.com(a)", atModel.getRoot().getChildAt(0).getChildAt(0).getNodeName()); assertEquals(Alert.RISK_HIGH, atModel.getRoot().getChildAt(0).getChildAt(0).getRisk()); } @@ -291,8 +295,8 @@ void shouldDeleteUniqueAlert() { """ - Alerts - Medium: Alert A - - :https://www.example.com/a2 - - :https://www.example.net + - GET:https://www.example.com/a2 + - GET:https://www.example.net """, TextAlertTree.toString(atModel)); @@ -347,10 +351,10 @@ void shouldChangeUniqueAlert() { """ - Alerts - High: Alert A - - :https://www.example.com/a1 + - GET:https://www.example.com/a1 - Medium: Alert A - - :https://www.example.com/a2 - - :https://www.example.net + - GET:https://www.example.com/a2 + - GET:https://www.example.net """, TextAlertTree.toString(atModel)); @@ -415,6 +419,16 @@ private static Alert newAlert( alert.setUri(uri); alert.setAlertId(id); alert.setNodeName(nodeName); + + HistoryReference href = mock(HistoryReference.class); + given(href.getMethod()).willReturn("GET"); + try { + given(href.getURI()).willReturn(new URI(uri, true)); + } catch (Exception e) { + // Ignore + } + alert.setHistoryRef(href); + 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 a31522ed0c4..a44323c3e66 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.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -51,6 +53,7 @@ import org.zaproxy.zap.model.ParameterParser; import org.zaproxy.zap.model.StandardParameterParser; import org.zaproxy.zap.utils.I18N; +import org.zaproxy.zap.utils.ZapXmlConfiguration; class ExtensionAlertUnitTest { @@ -689,4 +692,79 @@ void shouldPrependAlertTagCorrectly(String value) { assertEquals(ORIGINAL_REF, alert2.getReference()); assertEquals(ORIGINAL_TAG, alert2.getTags()); } + + @Test + void shouldIdentifySystemicAlerts() { + // Given + extAlert.getAlertParam().load(new ZapXmlConfiguration()); + extAlert.getAlertParam().setSystemicLimit(3); + + Alert a1 = + newAlert( + 1, + 0, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=1"); + Alert a2 = + newAlert( + 1, + 1, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=2"); + Alert a3 = + newAlert( + 1, + 2, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=3"); + Alert a4 = + newAlert( + 1, + 3, + "Alert A", + "https://www.example.com(a)", + "https://www.example.com?a=4"); + + a1.setTags(Map.of("SYSTEMIC", "true")); + a2.setTags(Map.of("SYSTEMIC", "true")); + a3.setTags(Map.of("SYSTEMIC", "true")); + a4.setTags(Map.of("SYSTEMIC", "true")); + + // When + boolean b1 = extAlert.isOverSystemicLimit(a1); + boolean b2 = extAlert.isOverSystemicLimit(a2); + boolean b3 = extAlert.isOverSystemicLimit(a3); + boolean b4 = extAlert.isOverSystemicLimit(a4); + + // Then + assertTrue(a1.isSystemic()); + assertTrue(a2.isSystemic()); + assertTrue(a3.isSystemic()); + assertTrue(a4.isSystemic()); + assertFalse(b1); + assertFalse(b2); + assertFalse(b3); + assertTrue(b4); + } + + private static Alert newAlert(int pluginId, int id, String name, String nodeName, String uri) { + Alert alert = new Alert(pluginId, Alert.RISK_MEDIUM, Alert.RISK_MEDIUM, name); + alert.setUri(uri); + alert.setAlertId(id); + alert.setNodeName(nodeName); + + HistoryReference href = mock(HistoryReference.class); + given(href.getMethod()).willReturn("GET"); + try { + given(href.getURI()).willReturn(new URI(uri, true)); + } catch (Exception e) { + // Ignore + } + alert.setHistoryRef(href); + + return alert; + } }