From 08514f017d5ad81d370bc05ee88117ba153de440 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 2 Jun 2026 20:05:44 +0200 Subject: [PATCH] SONARPY-4248 Add support for bandit's # nosec directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend NoSonarInfoParser to recognize `# nosec` comments. A bare `# nosec` (with or without trailing description text) is parsed the same way as bare `# NOSONAR` / `# noqa`, producing a NoSonarLineInfo with an empty suppressed-rule-key set — which the existing NoSonarIssueFilter already treats as "suppress all rules on this line". So e.g. S4790 is silenced when a `# nosec` is added to the offending line, matching bandit's own semantics. Bandit also accepts test IDs (e.g. `# nosec B303, B607`). Those are not parsed here: the repo has no Bandit-ID → Sonar-rule mapping, and any text after `nosec` is treated as a free-form description. Narrowing bare `# nosec` to security-typed rules only is tracked as a follow-up on SONARPY-4248. --- .../nosonar/NoSonarLineInfoCollectorTest.java | 18 ++++++++++++ .../python/api/nosonar/NoSonarInfoParser.java | 18 ++++++++++++ .../api/nosonar/NoSonarInfoParserTest.java | 28 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/python-commons/src/test/java/org/sonar/plugins/python/nosonar/NoSonarLineInfoCollectorTest.java b/python-commons/src/test/java/org/sonar/plugins/python/nosonar/NoSonarLineInfoCollectorTest.java index 7fb2f6e07..82d7748d0 100644 --- a/python-commons/src/test/java/org/sonar/plugins/python/nosonar/NoSonarLineInfoCollectorTest.java +++ b/python-commons/src/test/java/org/sonar/plugins/python/nosonar/NoSonarLineInfoCollectorTest.java @@ -185,6 +185,24 @@ private static Stream provideCollectorParameters() { Set.of(1, 2, 3, 4, 5), "", "" + ), + Arguments.of(""" + import hashlib + hashlib.md5(b"x").hexdigest() # nosec + """, + Map.of(2, new NoSonarLineInfo(Set.of(), "")), + Set.of(2), + "", + "" + ), + Arguments.of(""" + import hashlib + hashlib.md5(b"x").hexdigest() # nosec B303 legacy hash + """, + Map.of(2, new NoSonarLineInfo(Set.of(), "B303 legacy hash")), + Set.of(2), + "", + "" ) ); } diff --git a/python-frontend/src/main/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParser.java b/python-frontend/src/main/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParser.java index 4f80be0f4..791cd730c 100644 --- a/python-frontend/src/main/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParser.java +++ b/python-frontend/src/main/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParser.java @@ -33,14 +33,19 @@ public class NoSonarInfoParser { private static final String NOQA_PATTERN_REGEX = "^#\\s*noqa(?::\\s*(.+))?(?:[\\s;:].*)?"; private static final String NOSONAR_PREFIX_REGEX = "^#\\s*NOSONAR(\\W.*)?"; private static final String NOSONAR_PATTERN_REGEX = "^#\\s*NOSONAR(?:\\s*\\(([^)]*)\\))?($|\\s.*)"; + // Bandit `# nosec`. Anything after `nosec` is treated as a free-form description; rule IDs + // are not parsed, so every `# nosec` suppresses all issues on the line. + private static final String NOSEC_PATTERN_REGEX = "(?i)^#\\s*nosec\\b[:\\s]*(.*)"; private static final String RULE_KEY_PATTERN_REGEX = "^[a-zA-Z0-9]+$"; private final Pattern noSonarPattern; private final Pattern noQaPattern; + private final Pattern noSecPattern; public NoSonarInfoParser() { noSonarPattern = Pattern.compile(NOSONAR_PATTERN_REGEX); noQaPattern = Pattern.compile(NOQA_PATTERN_REGEX); + noSecPattern = Pattern.compile(NOSEC_PATTERN_REGEX); } public boolean isInvalidIssueSuppressionComment(String commentsLine) { @@ -94,6 +99,10 @@ public static boolean isValidNoQa(String noSonarCommentLine) { return noSonarCommentLine.matches(NOQA_PATTERN_REGEX); } + public static boolean isValidNoSec(String commentLine) { + return commentLine.matches(NOSEC_PATTERN_REGEX); + } + public Optional parse(String commentLine) { var rules = new HashSet(); StringBuilder concatenatedCommentBuilder = new StringBuilder(); @@ -132,6 +141,9 @@ private NoSonarLineInfo parseComment(String commentLine) { .filter(Predicate.not(String::isEmpty)) .forEach(rules::add); comment = parseNoQaComment(commentLine); + } else if (isValidNoSec(commentLine)) { + // `# nosec` always suppresses all rules on the line; we do not parse Bandit IDs. + comment = parseNoSecComment(commentLine); } else { return null; } @@ -174,6 +186,12 @@ private String parseNoQaComment(String noSonarCommentLine) { return getTruncatedCommentString(noQaPattern, noSonarCommentLine).strip(); } + private String parseNoSecComment(String noSecCommentLine) { + var raw = getPatternGroup(1, noSecPattern, noSecCommentLine); + var truncated = raw.length() > MAX_COMMENT_LENGTH ? raw.substring(0, MAX_COMMENT_LENGTH) : raw; + return truncated.strip(); + } + private static String getParamsString(Pattern pattern, String noSonarCommentLine) { return getPatternGroup(1, pattern, noSonarCommentLine); } diff --git a/python-frontend/src/test/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParserTest.java b/python-frontend/src/test/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParserTest.java index afde41140..53de781a4 100644 --- a/python-frontend/src/test/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParserTest.java +++ b/python-frontend/src/test/java/org/sonar/plugins/python/api/nosonar/NoSonarInfoParserTest.java @@ -130,6 +130,34 @@ private static Stream provideParserParameters() { Arguments.of( "# noqa; noqa; noqa; noqa; noqa", new NoSonarLineInfo(Set.of()) + ), + Arguments.of( + "# nosec", + new NoSonarLineInfo(Set.of()) + ), + Arguments.of( + "# nosec B101", + new NoSonarLineInfo(Set.of(), "B101") + ), + Arguments.of( + "# nosec B101, B102 reason text", + new NoSonarLineInfo(Set.of(), "B101, B102 reason text") + ), + Arguments.of( + "# nosec: it's fine", + new NoSonarLineInfo(Set.of(), "it's fine") + ), + Arguments.of( + "# NOSEC", + new NoSonarLineInfo(Set.of()) + ), + Arguments.of( + "# nosec a very long comment that I don't want to keep that long because there is more than 50 characters", + new NoSonarLineInfo(Set.of(), "a very long comment that I don't want to keep that") + ), + Arguments.of( + "# some text # nosec", + new NoSonarLineInfo(Set.of()) ) ); }