Skip to content

Commit 3714449

Browse files
authored
Merge pull request zaproxy#9097 from psiinon/alerts/systemic2
Systemic alert support
2 parents 5f467b7 + 9e8f3f8 commit 3714449

File tree

11 files changed

+242
-30
lines changed

11 files changed

+242
-30
lines changed

zap/src/main/java/org/parosproxy/paros/core/scanner/Alert.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
// ZAP: 2023/09/12 Add NUMBER_RISKS convenience constant.
7070
// ZAP: 2023/11/14 When setting CWE also add a CWE alert tag with an appropriate URL.
7171
// ZAP: 2025/10/01 Added support for nodeName.
72+
// ZAP: 2025/10/16 Added support for systemic alerts.
7273
package org.parosproxy.paros.core.scanner;
7374

7475
import java.net.URL;
@@ -199,6 +200,8 @@ public static Source getSource(int id) {
199200
private static final String CWE_KEY = "CWE-";
200201
private static final String CWE_URL_BASE = "https://cwe.mitre.org/data/definitions/";
201202

203+
private static final String SYSTEMIC_KEY = "SYSTEMIC";
204+
202205
private int alertId = -1; // ZAP: Changed default alertId
203206
private int historyId;
204207
private int pluginId = -1;
@@ -231,6 +234,7 @@ public static Source getSource(int id) {
231234
private String alertRef = "";
232235
private String nodeName;
233236
private Map<String, String> tags = Collections.emptyMap();
237+
private Boolean systemic;
234238

235239
public Alert(int pluginId) {
236240
this.pluginId = pluginId;
@@ -1090,6 +1094,18 @@ public void setNodeName(String nodeName) {
10901094
this.nodeName = nodeName;
10911095
}
10921096

1097+
/**
1098+
* Returns true if the alert has the "SYSTEMIC" tag
1099+
*
1100+
* @since 2.17.0
1101+
*/
1102+
public boolean isSystemic() {
1103+
if (systemic == null) {
1104+
systemic = this.getTags().containsKey(SYSTEMIC_KEY);
1105+
}
1106+
return systemic;
1107+
}
1108+
10931109
/**
10941110
* Returns a new alert builder.
10951111
*

zap/src/main/java/org/zaproxy/zap/extension/alert/AlertNode.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class AlertNode extends DefaultMutableTreeNode {
3434
private String nodeName = null;
3535
private int risk = -1;
3636
private Alert alert;
37+
private boolean systemic;
3738

3839
public AlertNode(int risk, String nodeName) {
3940
this(risk, nodeName, null);
@@ -143,9 +144,6 @@ public int findIndex(AlertNode aChild) {
143144

144145
@Override
145146
public String toString() {
146-
if (this.getChildCount() > 1) {
147-
return nodeName + " (" + this.getChildCount() + ")";
148-
}
149147
return nodeName;
150148
}
151149

@@ -161,6 +159,14 @@ public int getRisk() {
161159
return risk;
162160
}
163161

162+
public boolean isSystemic() {
163+
return systemic;
164+
}
165+
166+
public void setSystemic(boolean systemic) {
167+
this.systemic = systemic;
168+
}
169+
164170
private static class AlertNodeComparatorWrapper implements Comparator<TreeNode> {
165171

166172
private final Comparator<AlertNode> comparator;

zap/src/main/java/org/zaproxy/zap/extension/alert/AlertPanel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ public void setLinkWithSitesTreeSelection(boolean enabled) {
359359
*/
360360
private AlertTreeModel getLinkWithSitesTreeModel() {
361361
if (linkWithSitesTreeModel == null) {
362-
linkWithSitesTreeModel = new AlertTreeModel();
362+
linkWithSitesTreeModel = new AlertTreeModel(this.extension);
363363
}
364364
return linkWithSitesTreeModel;
365365
}

zap/src/main/java/org/zaproxy/zap/extension/alert/AlertParam.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,21 @@ public class AlertParam extends AbstractParam {
3737

3838
private static final String PARAM_OVERRIDES_FILENAME = PARAM_BASE_KEY + ".overridesFilename";
3939

40+
private static final String PARAM_SYSTEMIC_LIMIT = PARAM_BASE_KEY + ".systemicLimit";
41+
4042
private static final int DEFAULT_MAXIMUM_INSTANCES = 20;
4143

44+
private static final int DEFAULT_SYSTEMIC_LIMIT = 0;
45+
4246
/**
4347
* The number of maximum instances of each vulnerability included in a report.
4448
*
4549
* <p>Default is {@value #DEFAULT_MAXIMUM_INSTANCES}.
4650
*/
4751
private int maximumInstances = DEFAULT_MAXIMUM_INSTANCES;
4852

53+
private int systemicLimit = DEFAULT_SYSTEMIC_LIMIT;
54+
4955
private boolean mergeRelatedIssues = true;
5056

5157
private String overridesFilename;
@@ -58,6 +64,7 @@ public class AlertParam extends AbstractParam {
5864
@Override
5965
protected void parse() {
6066
maximumInstances = getInt(PARAM_MAXIMUM_INSTANCES, DEFAULT_MAXIMUM_INSTANCES);
67+
systemicLimit = getInt(PARAM_SYSTEMIC_LIMIT, DEFAULT_SYSTEMIC_LIMIT);
6168
mergeRelatedIssues = getBoolean(PARAM_MERGE_RELATED_ISSUES, true);
6269
overridesFilename = getString(PARAM_OVERRIDES_FILENAME, "");
6370
}
@@ -86,6 +93,20 @@ public int getMaximumInstances() {
8693
return maximumInstances;
8794
}
8895

96+
public int getSystemicLimit() {
97+
return systemicLimit;
98+
}
99+
100+
public void setSystemicLimit(int systemicLimit) {
101+
int newValue = Math.max(0, systemicLimit);
102+
103+
if (this.systemicLimit != newValue) {
104+
this.systemicLimit = newValue;
105+
106+
getConfig().setProperty(PARAM_SYSTEMIC_LIMIT, this.systemicLimit);
107+
}
108+
}
109+
89110
public boolean isMergeRelatedIssues() {
90111
return mergeRelatedIssues;
91112
}

zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeCellRenderer.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javax.swing.ImageIcon;
2424
import javax.swing.JTree;
2525
import javax.swing.tree.DefaultTreeCellRenderer;
26+
import org.parosproxy.paros.Constant;
2627
import org.zaproxy.zap.utils.DisplayUtils;
2728
import org.zaproxy.zap.view.SiteMapTreeCellRenderer;
2829

@@ -63,8 +64,7 @@ public Component getTreeCellRendererComponent(
6364

6465
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
6566

66-
if (value instanceof AlertNode) {
67-
AlertNode alertNode = (AlertNode) value;
67+
if (value instanceof AlertNode alertNode) {
6868
if (alertNode.isRoot()) {
6969
if (expanded) {
7070
this.setIcon(FOLDER_OPEN_ICON);
@@ -76,6 +76,17 @@ public Component getTreeCellRendererComponent(
7676
} else {
7777
this.setIcon(LEAF_ICON);
7878
}
79+
80+
if (alertNode.getChildCount() > 1) {
81+
if (alertNode.isSystemic()) {
82+
this.setText(
83+
Constant.messages.getString("alert.label.namesystemic", alertNode));
84+
} else {
85+
this.setText(
86+
Constant.messages.getString(
87+
"alert.label.namecount", alertNode, alertNode.getChildCount()));
88+
}
89+
}
7990
}
8091
return this;
8192
}

zap/src/main/java/org/zaproxy/zap/extension/alert/AlertTreeModel.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,15 @@ class AlertTreeModel extends DefaultTreeModel {
4141

4242
private static final Logger LOGGER = LogManager.getLogger(AlertTreeModel.class);
4343

44-
AlertTreeModel() {
44+
private ExtensionAlert ext;
45+
46+
AlertTreeModel(ExtensionAlert ext) {
4547
super(
4648
new AlertNode(
4749
-1,
4850
Constant.messages.getString("alerts.tree.title"),
4951
GROUP_ALERT_CHILD_COMPARATOR));
52+
this.ext = ext;
5053
}
5154

5255
void addPath(final Alert alert) {
@@ -215,6 +218,14 @@ private AlertNode addLeaf(AlertNode parent, String nodeName, Alert alert) {
215218
needle.setAlert(alert);
216219
int idx = parent.findIndex(needle);
217220
if (idx < 0) {
221+
// Not a duplicate alert
222+
if (ext.isOverSystemicLimit(alert)) {
223+
if (!parent.isSystemic()) {
224+
parent.setSystemic(true);
225+
nodeChanged(parent);
226+
}
227+
return null;
228+
}
218229
idx = -(idx + 1);
219230
parent.insert(needle, idx);
220231
nodesWereInserted(parent, new int[] {idx});

zap/src/main/java/org/zaproxy/zap/extension/alert/ExtensionAlert.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import java.util.SortedSet;
3434
import java.util.TreeSet;
3535
import java.util.Vector;
36+
import java.util.concurrent.ConcurrentHashMap;
37+
import java.util.concurrent.atomic.AtomicInteger;
3638
import javax.swing.JTree;
3739
import javax.swing.tree.TreePath;
3840
import org.apache.commons.httpclient.URIException;
@@ -94,6 +96,9 @@ public class ExtensionAlert extends ExtensionAdaptor
9496
private Properties alertOverrides = new Properties();
9597
private AlertAddDialog dialogAlertAdd;
9698

99+
private Map<String, Map<String, AtomicInteger>> siteToSystemicAlertMap =
100+
new ConcurrentHashMap<>();
101+
97102
public ExtensionAlert() {
98103
super(NAME);
99104
this.setOrder(27);
@@ -170,7 +175,7 @@ private OptionsAlertPanel getOptionsPanel() {
170175
return optionsPanel;
171176
}
172177

173-
private AlertParam getAlertParam() {
178+
protected AlertParam getAlertParam() {
174179
if (alertParam == null) {
175180
alertParam = new AlertParam();
176181
}
@@ -502,14 +507,14 @@ AlertPanel getAlertPanel() {
502507

503508
AlertTreeModel getTreeModel() {
504509
if (treeModel == null) {
505-
treeModel = new AlertTreeModel();
510+
treeModel = new AlertTreeModel(this);
506511
}
507512
return treeModel;
508513
}
509514

510515
private AlertTreeModel getFilteredTreeModel() {
511516
if (filteredTreeModel == null) {
512-
filteredTreeModel = new AlertTreeModel();
517+
filteredTreeModel = new AlertTreeModel(this);
513518
}
514519
return filteredTreeModel;
515520
}
@@ -689,11 +694,12 @@ public void run() {
689694
}
690695

691696
private void sessionChangedEventHandler(Session session) {
692-
setTreeModel(new AlertTreeModel());
697+
setTreeModel(new AlertTreeModel(this));
693698

694699
treeModel = null;
695700
filteredTreeModel = null;
696701
hrefs = new HashMap<>();
702+
siteToSystemicAlertMap = new ConcurrentHashMap<>();
697703

698704
if (session == null) {
699705
// Null session indicated we're shutting down
@@ -1259,4 +1265,31 @@ public boolean supportsLowMemory() {
12591265
public boolean isNewAlert(Alert alertToCheck) {
12601266
return (getTreeModel().getAlertNode(alertToCheck) == null);
12611267
}
1268+
1269+
/**
1270+
* Returns true if the given alert is over the systemic limit. If the alert is systemic then it
1271+
* will increment the count of that type of alert.
1272+
*
1273+
* @since 2.17.0
1274+
*/
1275+
public boolean isOverSystemicLimit(Alert alert) {
1276+
if (alert == null || !alert.isSystemic()) {
1277+
return false;
1278+
}
1279+
try {
1280+
// Always count locally, even if the systemicLimit is zero as that could be changed
1281+
Map<String, AtomicInteger> m =
1282+
siteToSystemicAlertMap.computeIfAbsent(
1283+
SessionStructure.getHostName(alert.getMsgUri()),
1284+
a -> new ConcurrentHashMap<>());
1285+
int count =
1286+
m.computeIfAbsent(alert.getAlertRef(), a -> new AtomicInteger())
1287+
.incrementAndGet();
1288+
int limit = getAlertParam().getSystemicLimit();
1289+
return limit > 0 && count > limit;
1290+
} catch (URIException e) {
1291+
// Ignore
1292+
}
1293+
return false;
1294+
}
12621295
}

zap/src/main/java/org/zaproxy/zap/extension/alert/OptionsAlertPanel.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public class OptionsAlertPanel extends AbstractParamPanel {
6161
*/
6262
private ZapNumberSpinner maxInstances;
6363

64+
private ZapNumberSpinner systemicLimit;
65+
6466
private JCheckBox mergeRelatedIssues;
6567
private JTextField overridesFilename;
6668

@@ -72,15 +74,23 @@ public OptionsAlertPanel() {
7274

7375
JPanel panel = new JPanel(new GridBagLayout());
7476
panel.setBorder(new EmptyBorder(2, 2, 2, 2));
77+
int y = 0;
7578

7679
panel.add(
77-
getMergeRelatedIssues(), LayoutHelper.getGBC(0, 0, 2, 1.0, new Insets(2, 2, 2, 2)));
80+
getMergeRelatedIssues(),
81+
LayoutHelper.getGBC(0, y++, 2, 1.0, new Insets(2, 2, 2, 2)));
7882

7983
JLabel maxInstancesLabel =
8084
new JLabel(Constant.messages.getString("alert.optionspanel.label.maxinstances"));
8185
maxInstancesLabel.setLabelFor(getMaxInstances());
82-
panel.add(maxInstancesLabel, LayoutHelper.getGBC(0, 1, 1, 1.0, new Insets(2, 2, 2, 2)));
83-
panel.add(getMaxInstances(), LayoutHelper.getGBC(1, 1, 1, 1.0, new Insets(2, 2, 2, 2)));
86+
panel.add(maxInstancesLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2)));
87+
panel.add(getMaxInstances(), LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2)));
88+
89+
JLabel systemicLimitLabel =
90+
new JLabel(Constant.messages.getString("alert.optionspanel.label.systemiclimit"));
91+
systemicLimitLabel.setLabelFor(getSystemicLimit());
92+
panel.add(systemicLimitLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2)));
93+
panel.add(getSystemicLimit(), LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2)));
8494

8595
JButton overridesButton =
8696
new JButton(
@@ -94,8 +104,8 @@ public OptionsAlertPanel() {
94104
overridesPanel.add(getOverridesFilename());
95105
overridesPanel.add(overridesButton);
96106

97-
panel.add(overridesLabel, LayoutHelper.getGBC(0, 2, 1, 1.0, new Insets(2, 2, 2, 2)));
98-
panel.add(overridesPanel, LayoutHelper.getGBC(1, 2, 1, 1.0, new Insets(2, 2, 2, 2)));
107+
panel.add(overridesLabel, LayoutHelper.getGBC(0, y, 1, 1.0, new Insets(2, 2, 2, 2)));
108+
panel.add(overridesPanel, LayoutHelper.getGBC(1, y++, 1, 1.0, new Insets(2, 2, 2, 2)));
99109

100110
add(panel);
101111
}
@@ -123,6 +133,13 @@ private ZapNumberSpinner getMaxInstances() {
123133
return maxInstances;
124134
}
125135

136+
private ZapNumberSpinner getSystemicLimit() {
137+
if (systemicLimit == null) {
138+
systemicLimit = new ZapNumberSpinner();
139+
}
140+
return systemicLimit;
141+
}
142+
126143
private JTextField getOverridesFilename() {
127144
if (overridesFilename == null) {
128145
overridesFilename = new JTextField(20);
@@ -136,6 +153,7 @@ public void initParam(Object obj) {
136153
final AlertParam param = options.getParamSet(AlertParam.class);
137154

138155
getMaxInstances().setValue(Integer.valueOf(param.getMaximumInstances()));
156+
getSystemicLimit().setValue(param.getSystemicLimit());
139157
getMergeRelatedIssues().setSelected(param.isMergeRelatedIssues());
140158
getMaxInstances().setEditable(param.isMergeRelatedIssues());
141159
getOverridesFilename().setText(param.getOverridesFilename());
@@ -160,6 +178,7 @@ public void saveParam(Object obj) throws Exception {
160178
final AlertParam param = options.getParamSet(AlertParam.class);
161179

162180
param.setMaximumInstances(getMaxInstances().getValue());
181+
param.setSystemicLimit(getSystemicLimit().getValue());
163182
param.setMergeRelatedIssues(getMergeRelatedIssues().isSelected());
164183
param.setOverridesFilename(getOverridesFilename().getText());
165184
}

zap/src/main/resources/org/zaproxy/zap/resources/Messages.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ alert.label.cweid = CWE ID:
9191
alert.label.desc = Description:
9292
alert.label.evidence = Evidence:
9393
alert.label.inputvector = Input Vector:
94+
alert.label.namecount = {0} ({1})
95+
alert.label.namesystemic = {0} (Systemic)
9496
alert.label.other = Other Info:
9597
alert.label.parameter = Parameter:
9698
alert.label.ref = Reference:
@@ -105,6 +107,7 @@ alert.optionspanel.button.overridesFilename = Select...
105107
alert.optionspanel.label.maxinstances = Max Alert Instances in Report:
106108
alert.optionspanel.label.mergerelated = Merge related alerts in report
107109
alert.optionspanel.label.overridesFilename = Alert Overrides File:
110+
alert.optionspanel.label.systemiclimit = Systemic Limit:
108111
alert.optionspanel.name = Alerts
109112
alert.optionspanel.warn.badOverridesFilename = Invalid Alert Overrides File
110113
alert.source.active = Active

0 commit comments

Comments
 (0)