Skip to content

Commit 33767dc

Browse files
authored
Merge pull request #6587 from psiinon/pscanrules/zapcheck
pscanrules: Add ZAP is out of date rule
2 parents f5929ec + 93297d7 commit 33767dc

File tree

6 files changed

+349
-0
lines changed

6 files changed

+349
-0
lines changed

addOns/pscanrules/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
## Unreleased
77
### Added
88
- The Reverse Tabnabbing and Retrieved from Cache scan rules now have CWE references.
9+
- A ZAP is Out of Date rule.
910

1011
## [65] - 2025-06-20
1112
### Added

addOns/pscanrules/src/main/java/org/zaproxy/zap/extension/pscanrules/ExtensionPscanRules.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
package org.zaproxy.zap.extension.pscanrules;
2121

2222
import org.parosproxy.paros.Constant;
23+
import org.parosproxy.paros.control.Control.Mode;
2324
import org.parosproxy.paros.extension.ExtensionAdaptor;
25+
import org.parosproxy.paros.extension.ExtensionHook;
26+
import org.parosproxy.paros.extension.SessionChangedListener;
27+
import org.parosproxy.paros.model.Session;
2428

2529
/**
2630
* A null extension just to cause the message bundle and help file to get loaded
@@ -48,4 +52,26 @@ public String getDescription() {
4852
public boolean canUnload() {
4953
return true;
5054
}
55+
56+
@Override
57+
public void hook(ExtensionHook extensionHook) {
58+
extensionHook.addSessionListener(new PscanSessionChangedListener());
59+
}
60+
61+
private static class PscanSessionChangedListener implements SessionChangedListener {
62+
63+
@Override
64+
public void sessionChanged(Session session) {
65+
ZapVersionScanRule.clear();
66+
}
67+
68+
@Override
69+
public void sessionAboutToChange(Session session) {}
70+
71+
@Override
72+
public void sessionScopeChanged(Session session) {}
73+
74+
@Override
75+
public void sessionModeChanged(Mode mode) {}
76+
}
5177
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2025 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.zap.extension.pscanrules;
21+
22+
import java.time.LocalDate;
23+
import java.time.Period;
24+
import java.time.YearMonth;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Set;
28+
import org.apache.commons.lang3.reflect.MethodUtils;
29+
import org.apache.logging.log4j.LogManager;
30+
import org.apache.logging.log4j.Logger;
31+
import org.parosproxy.paros.Constant;
32+
import org.parosproxy.paros.control.Control;
33+
import org.parosproxy.paros.core.scanner.Alert;
34+
import org.parosproxy.paros.network.HttpMessage;
35+
import org.zaproxy.zap.Version;
36+
import org.zaproxy.zap.extension.autoupdate.ExtensionAutoUpdate;
37+
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
38+
39+
public class ZapVersionScanRule extends PluginPassiveScanner implements CommonPassiveScanRuleInfo {
40+
41+
private static final String MESSAGE_PREFIX = "pscanrules.zapversion.";
42+
43+
private static final int PLUGIN_ID = 10116;
44+
45+
private static final Logger LOGGER = LogManager.getLogger(ZapVersionScanRule.class);
46+
47+
/** Used to make sure we only raise one alert per host. */
48+
private static Set<String> hosts = new HashSet<>();
49+
50+
private static String latestVersionStr;
51+
private static Integer risk;
52+
53+
@Override
54+
public void scanHttpRequestSend(HttpMessage msg, int id) {
55+
try {
56+
int risk = getRisk();
57+
if (risk <= 0) {
58+
return;
59+
}
60+
String host = msg.getRequestHeader().getURI().getHost();
61+
synchronized (hosts) {
62+
if (hosts.contains(host)) {
63+
return;
64+
}
65+
hosts.add(host);
66+
}
67+
buildAlert(risk, getLatestVersionStr()).raise();
68+
} catch (Exception e) {
69+
LOGGER.debug(e.getMessage(), e);
70+
}
71+
}
72+
73+
private int getRisk() {
74+
if (risk != null) {
75+
return risk;
76+
}
77+
if (Constant.isDevBuild()) {
78+
// Assume the user is keeping this up to date
79+
risk = -1;
80+
return risk;
81+
}
82+
83+
if (Constant.isDailyBuild()) {
84+
risk = getDateRisk(Constant.PROGRAM_VERSION, LocalDate.now());
85+
return risk;
86+
}
87+
88+
String latestVersion = getLatestVersionStr();
89+
if (latestVersion == null) {
90+
// No CFU req yet, might be done next time?
91+
return -1;
92+
}
93+
risk = getVersionRisk(Constant.PROGRAM_VERSION, latestVersion);
94+
return risk;
95+
}
96+
97+
/**
98+
* Returns the risk based on 2 semantic version strings like "2.16.1"
99+
*
100+
* @param currentVer the version of ZAP being run
101+
* @param latestVer the latest ZAP version based on the Check For Updates call
102+
* @return the risk, where <= 0: no risk, 1: low, 2: medium, 3: high
103+
*/
104+
protected static int getVersionRisk(String currentVer, String latestVer) {
105+
try {
106+
Version cv = new Version(currentVer);
107+
Version lv = new Version(latestVer);
108+
109+
if (cv.getMajorVersion() == lv.getMajorVersion()) {
110+
// Easy case, no major version difference
111+
return intToRisk(lv.getMinorVersion() - cv.getMinorVersion(), 0);
112+
}
113+
if (lv.getMajorVersion() - cv.getMajorVersion() > 1) {
114+
// 2+ major version differences
115+
return Alert.RISK_HIGH;
116+
}
117+
// 1 major version difference, always min of medium risk
118+
return intToRisk(lv.getMinorVersion(), Alert.RISK_MEDIUM);
119+
} catch (Exception e) {
120+
LOGGER.debug(e.getMessage(), e);
121+
return -1;
122+
}
123+
}
124+
125+
private static int intToRisk(int i, int minRisk) {
126+
if (i < minRisk) {
127+
return minRisk;
128+
}
129+
if (i > Alert.RISK_HIGH) {
130+
return Alert.RISK_HIGH;
131+
}
132+
return i;
133+
}
134+
135+
/**
136+
* Returns the risk based on 1 date-stamped version string like "D-2025-06-30"
137+
*
138+
* @param currentVer the date-stamped version of ZAP being run
139+
* @param today todays date (makes testing so much easier)
140+
* @return the risk, where <= 0: no risk, 1: low, 2: medium, 3: high
141+
*/
142+
protected static int getDateRisk(String currentVer, LocalDate today) {
143+
if (currentVer == null || !currentVer.startsWith("D-")) {
144+
return -1;
145+
}
146+
YearMonth yearMonth = YearMonth.parse(currentVer.substring(2, 9));
147+
LocalDate pastDate = yearMonth.atDay(1);
148+
return diffToRisk(Period.between(pastDate, today).getYears());
149+
}
150+
151+
private static int diffToRisk(int diff) {
152+
if (diff > Alert.RISK_HIGH) {
153+
return Alert.RISK_HIGH;
154+
}
155+
return diff;
156+
}
157+
158+
private static String getLatestVersionStr() {
159+
if (latestVersionStr == null) {
160+
ExtensionAutoUpdate ext =
161+
Control.getSingleton()
162+
.getExtensionLoader()
163+
.getExtension(ExtensionAutoUpdate.class);
164+
165+
if (ext != null) {
166+
try {
167+
Object ver = MethodUtils.invokeMethod(ext, true, "getLatestVersionNumber");
168+
169+
latestVersionStr = (String) ver;
170+
} catch (Exception e) {
171+
LOGGER.debug(e.getMessage(), e);
172+
}
173+
}
174+
}
175+
return latestVersionStr;
176+
}
177+
178+
private AlertBuilder buildAlert(int risk, String latest) {
179+
AlertBuilder ab =
180+
newAlert()
181+
.setRisk(risk)
182+
.setConfidence(Alert.CONFIDENCE_HIGH)
183+
.setDescription(Constant.messages.getString(MESSAGE_PREFIX + "desc"))
184+
.setSolution(Constant.messages.getString(MESSAGE_PREFIX + "soln"))
185+
.setReference(Constant.messages.getString(MESSAGE_PREFIX + "refs"))
186+
.setCweId(1104) // CWE-1104: Use of Unmaintained Third Party Components
187+
.setWascId(45); // WASC-45: Application Misconfiguration
188+
if (latest != null) {
189+
ab.setOtherInfo(Constant.messages.getString(MESSAGE_PREFIX + "otherinfo", latest));
190+
}
191+
192+
return ab;
193+
}
194+
195+
@Override
196+
public int getPluginId() {
197+
return PLUGIN_ID;
198+
}
199+
200+
@Override
201+
public String getName() {
202+
return Constant.messages.getString(MESSAGE_PREFIX + "name");
203+
}
204+
205+
@Override
206+
public List<Alert> getExampleAlerts() {
207+
return List.of(buildAlert(Alert.RISK_MEDIUM, null).build());
208+
}
209+
210+
protected static void clear() {
211+
hosts.clear();
212+
}
213+
}

addOns/pscanrules/src/main/javahelp/org/zaproxy/zap/extension/pscanrules/resources/help/contents/pscanrules.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,5 +575,22 @@ <H2 id="id-10056">X-Debug-Token Information Leak</H2>
575575
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/pscanrules/src/main/java/org/zaproxy/zap/extension/pscanrules/XDebugTokenScanRule.java">XDebugTokenScanRule.java</a>
576576
<br>
577577
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/10056/">10056</a>.
578+
579+
<H2 id="id-10116">ZAP is Out of Date</H2>
580+
This checks to make sure you are using the latest version of ZAP.<br>
581+
If your ZAP install is out of date then it will raise one alert on each domain with a severity based on how many versions
582+
out of date it is.<br>
583+
<ul>
584+
<li>1 version: Low Risk
585+
<li>2 versions: Medium Risk
586+
<li>3+ versions: High Risk
587+
<li>More that one year: High Risk (just for date stamped versions, such as the nightly or weekly releases)
588+
</ul>
589+
590+
<p>
591+
Latest code: <a href="https://github.com/zaproxy/zap-extensions/blob/main/addOns/pscanrules/src/main/java/org/zaproxy/zap/extension/pscanrules/ZapVersionScanRule.java">ZapVersionScanRule.java</a>
592+
<br>
593+
Alert ID: <a href="https://www.zaproxy.org/docs/alerts/10116/">10116</a>.
594+
578595
</BODY>
579596
</HTML>

addOns/pscanrules/src/main/resources/org/zaproxy/zap/extension/pscanrules/resources/Messages.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,9 @@ pscanrules.xpoweredbyheaderinfoleak.name = Server Leaks Information via "X-Power
422422
pscanrules.xpoweredbyheaderinfoleak.otherinfo.msg = The following X-Powered-By headers were also found:\n
423423
pscanrules.xpoweredbyheaderinfoleak.refs = https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/01-Information_Gathering/08-Fingerprint_Web_Application_Framework\nhttps://www.troyhunt.com/2012/02/shhh-dont-let-your-response-headers.html
424424
pscanrules.xpoweredbyheaderinfoleak.soln = Ensure that your web server, application server, load balancer, etc. is configured to suppress "X-Powered-By" headers.
425+
426+
pscanrules.zapversion.desc = The version of ZAP you are using to test your app is out of date and is no longer being updated.\nThe risk level is set based on how out of date your ZAP version is.
427+
pscanrules.zapversion.name = ZAP is Out of Date
428+
pscanrules.zapversion.otherinfo = The latest version of ZAP is {0}
429+
pscanrules.zapversion.refs = https://www.zaproxy.org/download/
430+
pscanrules.zapversion.soln = Download the latest version of ZAP from https://www.zaproxy.org/download/ and install it.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2025 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.zap.extension.pscanrules;
21+
22+
import static org.hamcrest.MatcherAssert.assertThat;
23+
import static org.hamcrest.Matchers.equalTo;
24+
import static org.hamcrest.Matchers.is;
25+
26+
import java.time.LocalDate;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.CsvSource;
30+
31+
class ZapVersionScanRuleUnitTest extends PassiveScannerTest<ZapVersionScanRule> {
32+
33+
LocalDate today = LocalDate.of(2025, 7, 10);
34+
35+
@Override
36+
protected ZapVersionScanRule createScanner() {
37+
return new ZapVersionScanRule();
38+
}
39+
40+
@Test
41+
void shouldHandleInvalidDateVersionStrings() {
42+
// Given / When / then
43+
assertThat(ZapVersionScanRule.getDateRisk("2020-01-01", today), is(equalTo(-1)));
44+
assertThat(ZapVersionScanRule.getDateRisk("d-2020-01-01", today), is(equalTo(-1)));
45+
assertThat(ZapVersionScanRule.getDateRisk("D_2020-01-01", today), is(equalTo(-1)));
46+
assertThat(ZapVersionScanRule.getDateRisk(null, today), is(equalTo(-1)));
47+
assertThat(ZapVersionScanRule.getDateRisk(null, null), is(equalTo(-1)));
48+
}
49+
50+
@ParameterizedTest
51+
@CsvSource(
52+
value = {
53+
"D-2030-03-29, -4",
54+
"D-2026-07-07, 0",
55+
"D-2025-07-06, 0",
56+
"D-2024-08-11, 0",
57+
"D-2024-07-30, 1",
58+
"D-2024-06-11, 1",
59+
"D-2020-01-01, 3",
60+
"D-2010-09-01, 3"
61+
})
62+
void shouldReturnExpectedDateRisks(String version, String riskStr) {
63+
assertThat(
64+
ZapVersionScanRule.getDateRisk(version, today),
65+
is(equalTo(Integer.parseInt(riskStr))));
66+
}
67+
68+
@ParameterizedTest
69+
@CsvSource(
70+
value = {
71+
"2.18.1, 2.16.0, 0",
72+
"2.16.1, 2.16.0, 0",
73+
"2.15.0, 2.16.0, 1",
74+
"2.14.1, 2.16.0, 2",
75+
"2.16.1, 3.1.0, 2",
76+
"2.8.0, 2.16.0, 3",
77+
"2.16.1, 3.5.0, 3",
78+
"2.14.1, 2.17.0, 3"
79+
})
80+
void shouldReturnExpectedVersionRisks(
81+
String currentVersion, String latestVersion, String riskStr) {
82+
assertThat(
83+
ZapVersionScanRule.getVersionRisk(currentVersion, latestVersion),
84+
is(equalTo(Integer.parseInt(riskStr))));
85+
}
86+
}

0 commit comments

Comments
 (0)