Skip to content

Commit 0d47334

Browse files
danparizherntBre
andauthored
[flake8-bandit] Support new PySNMP API paths (S508, S509) (#21374)
## Summary Updated `S508` (snmp-insecure-version) and `S509` (snmp-weak-cryptography) rules to support both old and new PySNMP API module paths. Previously, these rules only detected the old API path `pysnmp.hlapi.*`, but now they correctly detect all PySNMP API variants including `pysnmp.hlapi.asyncio.*`, `pysnmp.hlapi.v1arch.*`, `pysnmp.hlapi.v3arch.*`, and `pysnmp.hlapi.auth.*`. Fixes #21364 ## Problem Analysis The `S508` and `S509` rules used exact pattern matching on qualified names: - `S509` only matched `["pysnmp", "hlapi", "UsmUserData"]` - `S508` only matched `["pysnmp", "hlapi", "CommunityData"]` This meant that newer PySNMP API paths were not detected, such as: - `pysnmp.hlapi.asyncio.UsmUserData` - `pysnmp.hlapi.v3arch.asyncio.UsmUserData` - `pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData` - `pysnmp.hlapi.auth.UsmUserData` - Similar variants for `CommunityData` in `S508` Additionally, the old API path `pysnmp.hlapi.auth.*` was also missing from both rules. ## Approach Instead of exact pattern matching, both rules now check if: 1. The qualified name starts with `["pysnmp", "hlapi"]` 2. The qualified name ends with the target class name (`"UsmUserData"` for `S509`, `"CommunityData"` for `S508`) This flexible approach matches all PySNMP API paths without hardcoding each variant, making the rules more maintainable and future-proof. ## Test Plan Added comprehensive test cases to both `S508.py` and `S509.py` test files covering: - New API paths: `pysnmp.hlapi.asyncio.*`, `pysnmp.hlapi.v1arch.*`, `pysnmp.hlapi.v3arch.*` - Old API path: `pysnmp.hlapi.auth.*` - Both insecure and secure usage patterns All existing tests pass, and new snapshot tests were added and accepted. Manual verification confirms both rules correctly detect all PySNMP API variants. --------- Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
1 parent a8f7ccf commit 0d47334

File tree

8 files changed

+245
-8
lines changed

8 files changed

+245
-8
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_bandit/S508.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,31 @@
44
CommunityData("public", mpModel=1) # S508
55

66
CommunityData("public", mpModel=2) # OK
7+
8+
# New API paths
9+
import pysnmp.hlapi.asyncio
10+
import pysnmp.hlapi.v1arch
11+
import pysnmp.hlapi.v1arch.asyncio
12+
import pysnmp.hlapi.v1arch.asyncio.auth
13+
import pysnmp.hlapi.v3arch
14+
import pysnmp.hlapi.v3arch.asyncio
15+
import pysnmp.hlapi.v3arch.asyncio.auth
16+
import pysnmp.hlapi.auth
17+
18+
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19+
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20+
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21+
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22+
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23+
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24+
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25+
pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
26+
27+
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
28+
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
29+
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=2) # OK
30+
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=2) # OK
31+
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
32+
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=2) # OK
33+
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=2) # OK
34+
pysnmp.hlapi.auth.CommunityData("public", mpModel=2) # OK

crates/ruff_linter/resources/test/fixtures/flake8_bandit/S509.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,19 @@
55
auth_no_priv = UsmUserData("securityName", "authName") # S509
66

77
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
8+
9+
# New API paths
10+
import pysnmp.hlapi.asyncio
11+
import pysnmp.hlapi.v3arch.asyncio
12+
import pysnmp.hlapi.v3arch.asyncio.auth
13+
import pysnmp.hlapi.auth
14+
15+
pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16+
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17+
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18+
pysnmp.hlapi.auth.UsmUserData("user") # S509
19+
20+
pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
21+
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user", "authkey", "privkey") # OK
22+
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user", "authkey", "privkey") # OK
23+
pysnmp.hlapi.auth.UsmUserData("user", "authkey", "privkey") # OK

crates/ruff_linter/src/preview.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &Linter
270270
settings.preview.is_enabled()
271271
}
272272

273+
// https://github.com/astral-sh/ruff/pull/21374
274+
pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool {
275+
settings.preview.is_enabled()
276+
}
277+
273278
// https://github.com/astral-sh/ruff/pull/21395
274279
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
275280
settings.preview.is_enabled()

crates/ruff_linter/src/rules/flake8_bandit/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ mod tests {
104104
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
105105
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
106106
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
107+
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
108+
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
107109
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
108110
let snapshot = format!(
109111
"preview__{}_{}",

crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
44

55
use crate::Violation;
66
use crate::checkers::ast::Checker;
7+
use crate::preview::is_extended_snmp_api_path_detection_enabled;
78

89
/// ## What it does
910
/// Checks for uses of SNMPv1 or SNMPv2.
@@ -47,10 +48,17 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) {
4748
.semantic()
4849
.resolve_qualified_name(&call.func)
4950
.is_some_and(|qualified_name| {
50-
matches!(
51-
qualified_name.segments(),
52-
["pysnmp", "hlapi", "CommunityData"]
53-
)
51+
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
52+
matches!(
53+
qualified_name.segments(),
54+
["pysnmp", "hlapi", .., "CommunityData"]
55+
)
56+
} else {
57+
matches!(
58+
qualified_name.segments(),
59+
["pysnmp", "hlapi", "CommunityData"]
60+
)
61+
}
5462
})
5563
{
5664
if let Some(keyword) = call.arguments.find_keyword("mpModel") {

crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
44

55
use crate::Violation;
66
use crate::checkers::ast::Checker;
7+
use crate::preview::is_extended_snmp_api_path_detection_enabled;
78

89
/// ## What it does
910
/// Checks for uses of the SNMPv3 protocol without encryption.
@@ -47,10 +48,17 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) {
4748
.semantic()
4849
.resolve_qualified_name(&call.func)
4950
.is_some_and(|qualified_name| {
50-
matches!(
51-
qualified_name.segments(),
52-
["pysnmp", "hlapi", "UsmUserData"]
53-
)
51+
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
52+
matches!(
53+
qualified_name.segments(),
54+
["pysnmp", "hlapi", .., "UsmUserData"]
55+
)
56+
} else {
57+
matches!(
58+
qualified_name.segments(),
59+
["pysnmp", "hlapi", "UsmUserData"]
60+
)
61+
}
5462
})
5563
{
5664
checker.report_diagnostic(SnmpWeakCryptography, call.func.range());
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
3+
---
4+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
5+
--> S508.py:3:25
6+
|
7+
1 | from pysnmp.hlapi import CommunityData
8+
2 |
9+
3 | CommunityData("public", mpModel=0) # S508
10+
| ^^^^^^^^^
11+
4 | CommunityData("public", mpModel=1) # S508
12+
|
13+
14+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
15+
--> S508.py:4:25
16+
|
17+
3 | CommunityData("public", mpModel=0) # S508
18+
4 | CommunityData("public", mpModel=1) # S508
19+
| ^^^^^^^^^
20+
5 |
21+
6 | CommunityData("public", mpModel=2) # OK
22+
|
23+
24+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
25+
--> S508.py:18:46
26+
|
27+
16 | import pysnmp.hlapi.auth
28+
17 |
29+
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
30+
| ^^^^^^^^^
31+
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
32+
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
33+
|
34+
35+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
36+
--> S508.py:19:58
37+
|
38+
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
39+
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
40+
| ^^^^^^^^^
41+
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
42+
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
43+
|
44+
45+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
46+
--> S508.py:20:53
47+
|
48+
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
49+
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
50+
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
51+
| ^^^^^^^^^
52+
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
53+
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
54+
|
55+
56+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
57+
--> S508.py:21:45
58+
|
59+
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
60+
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
61+
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
62+
| ^^^^^^^^^
63+
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
64+
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
65+
|
66+
67+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
68+
--> S508.py:22:58
69+
|
70+
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
71+
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
72+
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
73+
| ^^^^^^^^^
74+
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
75+
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
76+
|
77+
78+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
79+
--> S508.py:23:53
80+
|
81+
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
82+
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
83+
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
84+
| ^^^^^^^^^
85+
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
86+
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
87+
|
88+
89+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
90+
--> S508.py:24:45
91+
|
92+
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
93+
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
94+
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
95+
| ^^^^^^^^^
96+
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
97+
|
98+
99+
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
100+
--> S508.py:25:43
101+
|
102+
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
103+
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
104+
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
105+
| ^^^^^^^^^
106+
26 |
107+
27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
108+
|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
3+
---
4+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
5+
--> S509.py:4:12
6+
|
7+
4 | insecure = UsmUserData("securityName") # S509
8+
| ^^^^^^^^^^^
9+
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
10+
|
11+
12+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
13+
--> S509.py:5:16
14+
|
15+
4 | insecure = UsmUserData("securityName") # S509
16+
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
17+
| ^^^^^^^^^^^
18+
6 |
19+
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
20+
|
21+
22+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
23+
--> S509.py:15:1
24+
|
25+
13 | import pysnmp.hlapi.auth
26+
14 |
27+
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
28+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29+
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
30+
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
31+
|
32+
33+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
34+
--> S509.py:16:1
35+
|
36+
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
37+
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
38+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39+
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
40+
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
41+
|
42+
43+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
44+
--> S509.py:17:1
45+
|
46+
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
47+
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
48+
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
49+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
51+
|
52+
53+
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
54+
--> S509.py:18:1
55+
|
56+
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
57+
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
58+
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
59+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60+
19 |
61+
20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
62+
|

0 commit comments

Comments
 (0)