From 8bc5ad856e23e69445fbce5c8c49b5863c69e183 Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Wed, 15 Jul 2026 05:03:06 -0400 Subject: [PATCH 1/2] checks: Mark justified partial-path subprocess calls as nosec B607 Address review feedback: instead of skipping Bandit B607 globally (which would also stop catching newly introduced violations), suppress it inline at each existing call site. These are developer, build, and test helpers that invoke known tools (git, gh, svn, msgfmt, mdl, man, grass) by name, intentionally resolved through PATH, the same rationale as the already-skipped B603. The g.region call in pygrass grid is addressed separately in the shell=True cleanup (#7703). --- locale/grass_po_stats.py | 2 +- python/grass/gunittest/reporters.py | 6 +++--- scripts/g.manual/g.manual.py | 2 +- utils/coverage_mapper.py | 2 +- utils/generate_last_commit_file.py | 2 +- utils/generate_release_notes.py | 8 ++++---- utils/md_isvalid.py | 4 ++-- utils/mkdocs.py | 8 ++++---- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/locale/grass_po_stats.py b/locale/grass_po_stats.py index 7e441627b24..bfa5c57e4da 100755 --- a/locale/grass_po_stats.py +++ b/locale/grass_po_stats.py @@ -112,7 +112,7 @@ def get_stats(languages, directory): fpref = flang.split("_")[0] # run msgfmt for statistics # TODO check if it's working on windows - process = subprocess.Popen( + process = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH ["msgfmt", "--statistics", os.path.join(directory, flang)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/python/grass/gunittest/reporters.py b/python/grass/gunittest/reporters.py index fde3d6800a6..8fc083b7844 100644 --- a/python/grass/gunittest/reporters.py +++ b/python/grass/gunittest/reporters.py @@ -176,7 +176,7 @@ def get_svn_revision(): """ # TODO: here should be starting directory # but now we are using current as starting - with subprocess.Popen( + with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH ["svnversion", "."], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) as p: stdout, stderr = p.communicate() @@ -199,7 +199,7 @@ def get_svn_info(): """ try: # TODO: introduce directory, not only current - with subprocess.Popen( + with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH ["svn", "info", ".", "--xml"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -254,7 +254,7 @@ def get_svn_path_authors(path, from_date=None): revision_range = "BASE:1" if from_date is None else "BASE:{%s}" % from_date try: # TODO: allow also usage of --limit - with subprocess.Popen( + with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH ["svn", "log", "--xml", "--revision", revision_range, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/scripts/g.manual/g.manual.py b/scripts/g.manual/g.manual.py index 58e93aea870..7c8fba47277 100755 --- a/scripts/g.manual/g.manual.py +++ b/scripts/g.manual/g.manual.py @@ -124,7 +124,7 @@ def start_man(entry): for ext in ["", ".gz", ".bz2"]: if Path(path + ext).exists(): - os.execlp("man", "man", path + ext) + os.execlp("man", "man", path + ext) # nosec B607: executable intentionally resolved via PATH grass.fatal(_("Error starting 'man' for '%s'") % path) grass.fatal(_("No manual page entry for '%s'") % entry) diff --git a/utils/coverage_mapper.py b/utils/coverage_mapper.py index 88fa30f9b54..ef03eacded8 100644 --- a/utils/coverage_mapper.py +++ b/utils/coverage_mapper.py @@ -6,7 +6,7 @@ def get_grass_config_path(): grass_config_path = None try: - grass_config_path = subprocess.run( + grass_config_path = subprocess.run( # nosec B607: executable intentionally resolved via PATH ["grass", "--config", "path"], capture_output=True, text=True, check=True ).stdout.rstrip() except OSError: diff --git a/utils/generate_last_commit_file.py b/utils/generate_last_commit_file.py index e9d91bad3a4..67d2a04fa71 100644 --- a/utils/generate_last_commit_file.py +++ b/utils/generate_last_commit_file.py @@ -56,7 +56,7 @@ def get_last_commit(src_dir): if ".html{}".format(join_sep) not in join_sep.join(files) + join_sep: continue rel_path = os.path.relpath(root) - process_result = subprocess.run( + process_result = subprocess.run( # nosec B607: executable intentionally resolved via PATH [ "git", "log", diff --git a/utils/generate_release_notes.py b/utils/generate_release_notes.py index 85312a0d027..d5768bae417 100755 --- a/utils/generate_release_notes.py +++ b/utils/generate_release_notes.py @@ -154,7 +154,7 @@ def adjust_after(lines): username = username.replace("@", "") if username in known_bot_names: continue - output = subprocess.run( + output = subprocess.run( # nosec B607: executable intentionally resolved via PATH ["gh", "api", f"users/{username}"], capture_output=True, text=True, @@ -203,7 +203,7 @@ def print_notes( def notes_from_gh_api(start_tag, end_tag, branch, categories, exclude): """Generate notes from GitHub API""" - text = subprocess.run( + text = subprocess.run( # nosec B607: executable intentionally resolved via PATH [ "gh", "api", @@ -255,7 +255,7 @@ def csv_to_dict(filename, key, value): def notes_from_git_log(start_tag, end_tag, categories, exclude): """Generate notes from git log""" - text = subprocess.run( + text = subprocess.run( # nosec B607: executable intentionally resolved via PATH ["git", "log", f"{start_tag}..{end_tag}", f"--pretty=format:{PRETTY_TEMPLATE}"], capture_output=True, text=True, @@ -337,7 +337,7 @@ def create_release_notes(args): end_tag = args.end_tag if not end_tag: # git log has default, but the others do not. - end_tag = subprocess.run( + end_tag = subprocess.run( # nosec B607: executable intentionally resolved via PATH ["git", "rev-parse", "--verify", "HEAD"], capture_output=True, text=True, diff --git a/utils/md_isvalid.py b/utils/md_isvalid.py index 56bf55f1e8f..1448e2cf64c 100644 --- a/utils/md_isvalid.py +++ b/utils/md_isvalid.py @@ -14,7 +14,7 @@ def check_md(filename): - with subprocess.Popen(["mdl", filename]) as p: + with subprocess.Popen(["mdl", filename]) as p: # nosec B607: executable intentionally resolved via PATH p.wait() @@ -29,7 +29,7 @@ def check_module(module): with open(tmp_file, "w") as fp: with subprocess.Popen([module, "--md-description"], stdout=fp) as p: p.wait() - with subprocess.Popen( + with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH [ "mdl", "--style", diff --git a/utils/mkdocs.py b/utils/mkdocs.py index a8a7d7848dc..1f76e398650 100644 --- a/utils/mkdocs.py +++ b/utils/mkdocs.py @@ -73,7 +73,7 @@ def get_version_branch(major_version, addons_git_repo_url): """ version_branch = f"grass{major_version}" if source_is_remote(): - branch = subprocess.Popen( + branch = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH [ "git", "ls-remote", @@ -110,7 +110,7 @@ def has_src_code_git(src_dir): actual_dir = Path.cwd() os.chdir(src_dir) try: - process_result = subprocess.run( + process_result = subprocess.run( # nosec B607: executable intentionally resolved via PATH [ "git", "log", @@ -399,7 +399,7 @@ def get_addon_path(base_url, pgm, major_version): try: with tempfile.TemporaryDirectory(dir=addons_base_dir) as tmpdir: tmp_clone_path = Path(tmpdir) / "grass-addons" - subprocess.call( + subprocess.call( # nosec B607: executable intentionally resolved via PATH [ "git", "clone", @@ -415,7 +415,7 @@ def get_addon_path(base_url, pgm, major_version): except (shutil.Error, OSError): if not grass_addons_dir.exists(): raise - addons_file_list = subprocess.Popen( + addons_file_list = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH ["git", "ls-tree", "--name-only", "-r", addons_branch], cwd=grass_addons_dir, stdout=subprocess.PIPE, From 943d08733e57bb485268a7ce5a8238d0010afecd Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Thu, 16 Jul 2026 06:21:00 -0400 Subject: [PATCH 2/2] checks: Clarify B607 nosec justifications per call site Name the specific tool in each suppression and state why the partial path is permanent: these are fixed external tools (git, gh, svn, svnversion, msgfmt, mdl, grass) with no portable absolute path, and g.manual launches the user's own system man. PATH resolution is required by design, so the exclusion cannot become stale. --- locale/grass_po_stats.py | 2 +- python/grass/gunittest/reporters.py | 6 +++--- scripts/g.manual/g.manual.py | 2 +- utils/coverage_mapper.py | 2 +- utils/generate_last_commit_file.py | 2 +- utils/generate_release_notes.py | 8 ++++---- utils/md_isvalid.py | 4 ++-- utils/mkdocs.py | 8 ++++---- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/locale/grass_po_stats.py b/locale/grass_po_stats.py index bfa5c57e4da..d2b29deb098 100755 --- a/locale/grass_po_stats.py +++ b/locale/grass_po_stats.py @@ -112,7 +112,7 @@ def get_stats(languages, directory): fpref = flang.split("_")[0] # run msgfmt for statistics # TODO check if it's working on windows - process = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + process = subprocess.Popen( # nosec B607: fixed external tool "msgfmt" with no portable absolute path ["msgfmt", "--statistics", os.path.join(directory, flang)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/python/grass/gunittest/reporters.py b/python/grass/gunittest/reporters.py index 8fc083b7844..47b223875f5 100644 --- a/python/grass/gunittest/reporters.py +++ b/python/grass/gunittest/reporters.py @@ -176,7 +176,7 @@ def get_svn_revision(): """ # TODO: here should be starting directory # but now we are using current as starting - with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + with subprocess.Popen( # nosec B607: fixed external tool "svnversion" with no portable absolute path ["svnversion", "."], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) as p: stdout, stderr = p.communicate() @@ -199,7 +199,7 @@ def get_svn_info(): """ try: # TODO: introduce directory, not only current - with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + with subprocess.Popen( # nosec B607: fixed external tool "svn" with no portable absolute path ["svn", "info", ".", "--xml"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -254,7 +254,7 @@ def get_svn_path_authors(path, from_date=None): revision_range = "BASE:1" if from_date is None else "BASE:{%s}" % from_date try: # TODO: allow also usage of --limit - with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + with subprocess.Popen( # nosec B607: fixed external tool "svn" with no portable absolute path ["svn", "log", "--xml", "--revision", revision_range, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/scripts/g.manual/g.manual.py b/scripts/g.manual/g.manual.py index 7c8fba47277..25dfdc201ab 100755 --- a/scripts/g.manual/g.manual.py +++ b/scripts/g.manual/g.manual.py @@ -124,7 +124,7 @@ def start_man(entry): for ext in ["", ".gz", ".bz2"]: if Path(path + ext).exists(): - os.execlp("man", "man", path + ext) # nosec B607: executable intentionally resolved via PATH + os.execlp("man", "man", path + ext) # nosec B607: runs the user's system "man" (PATH lookup intended) grass.fatal(_("Error starting 'man' for '%s'") % path) grass.fatal(_("No manual page entry for '%s'") % entry) diff --git a/utils/coverage_mapper.py b/utils/coverage_mapper.py index ef03eacded8..09a60cb65f3 100644 --- a/utils/coverage_mapper.py +++ b/utils/coverage_mapper.py @@ -6,7 +6,7 @@ def get_grass_config_path(): grass_config_path = None try: - grass_config_path = subprocess.run( # nosec B607: executable intentionally resolved via PATH + grass_config_path = subprocess.run( # nosec B607: fixed external tool "grass" with no portable absolute path ["grass", "--config", "path"], capture_output=True, text=True, check=True ).stdout.rstrip() except OSError: diff --git a/utils/generate_last_commit_file.py b/utils/generate_last_commit_file.py index 67d2a04fa71..91ea05cdfac 100644 --- a/utils/generate_last_commit_file.py +++ b/utils/generate_last_commit_file.py @@ -56,7 +56,7 @@ def get_last_commit(src_dir): if ".html{}".format(join_sep) not in join_sep.join(files) + join_sep: continue rel_path = os.path.relpath(root) - process_result = subprocess.run( # nosec B607: executable intentionally resolved via PATH + process_result = subprocess.run( # nosec B607: fixed external tool "git" with no portable absolute path [ "git", "log", diff --git a/utils/generate_release_notes.py b/utils/generate_release_notes.py index d5768bae417..e5a95d684aa 100755 --- a/utils/generate_release_notes.py +++ b/utils/generate_release_notes.py @@ -154,7 +154,7 @@ def adjust_after(lines): username = username.replace("@", "") if username in known_bot_names: continue - output = subprocess.run( # nosec B607: executable intentionally resolved via PATH + output = subprocess.run( # nosec B607: fixed external tool "gh" with no portable absolute path ["gh", "api", f"users/{username}"], capture_output=True, text=True, @@ -203,7 +203,7 @@ def print_notes( def notes_from_gh_api(start_tag, end_tag, branch, categories, exclude): """Generate notes from GitHub API""" - text = subprocess.run( # nosec B607: executable intentionally resolved via PATH + text = subprocess.run( # nosec B607: fixed external tool "gh" with no portable absolute path [ "gh", "api", @@ -255,7 +255,7 @@ def csv_to_dict(filename, key, value): def notes_from_git_log(start_tag, end_tag, categories, exclude): """Generate notes from git log""" - text = subprocess.run( # nosec B607: executable intentionally resolved via PATH + text = subprocess.run( # nosec B607: fixed external tool "git" with no portable absolute path ["git", "log", f"{start_tag}..{end_tag}", f"--pretty=format:{PRETTY_TEMPLATE}"], capture_output=True, text=True, @@ -337,7 +337,7 @@ def create_release_notes(args): end_tag = args.end_tag if not end_tag: # git log has default, but the others do not. - end_tag = subprocess.run( # nosec B607: executable intentionally resolved via PATH + end_tag = subprocess.run( # nosec B607: fixed external tool "git" with no portable absolute path ["git", "rev-parse", "--verify", "HEAD"], capture_output=True, text=True, diff --git a/utils/md_isvalid.py b/utils/md_isvalid.py index 1448e2cf64c..342c0430cc1 100644 --- a/utils/md_isvalid.py +++ b/utils/md_isvalid.py @@ -14,7 +14,7 @@ def check_md(filename): - with subprocess.Popen(["mdl", filename]) as p: # nosec B607: executable intentionally resolved via PATH + with subprocess.Popen(["mdl", filename]) as p: # nosec B607: fixed external tool "mdl" with no portable absolute path p.wait() @@ -29,7 +29,7 @@ def check_module(module): with open(tmp_file, "w") as fp: with subprocess.Popen([module, "--md-description"], stdout=fp) as p: p.wait() - with subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + with subprocess.Popen( # nosec B607: fixed external tool "mdl" with no portable absolute path [ "mdl", "--style", diff --git a/utils/mkdocs.py b/utils/mkdocs.py index 1f76e398650..1ce702eba92 100644 --- a/utils/mkdocs.py +++ b/utils/mkdocs.py @@ -73,7 +73,7 @@ def get_version_branch(major_version, addons_git_repo_url): """ version_branch = f"grass{major_version}" if source_is_remote(): - branch = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + branch = subprocess.Popen( # nosec B607: fixed external tool "git" with no portable absolute path [ "git", "ls-remote", @@ -110,7 +110,7 @@ def has_src_code_git(src_dir): actual_dir = Path.cwd() os.chdir(src_dir) try: - process_result = subprocess.run( # nosec B607: executable intentionally resolved via PATH + process_result = subprocess.run( # nosec B607: fixed external tool "git" with no portable absolute path [ "git", "log", @@ -399,7 +399,7 @@ def get_addon_path(base_url, pgm, major_version): try: with tempfile.TemporaryDirectory(dir=addons_base_dir) as tmpdir: tmp_clone_path = Path(tmpdir) / "grass-addons" - subprocess.call( # nosec B607: executable intentionally resolved via PATH + subprocess.call( # nosec B607: fixed external tool "git" with no portable absolute path [ "git", "clone", @@ -415,7 +415,7 @@ def get_addon_path(base_url, pgm, major_version): except (shutil.Error, OSError): if not grass_addons_dir.exists(): raise - addons_file_list = subprocess.Popen( # nosec B607: executable intentionally resolved via PATH + addons_file_list = subprocess.Popen( # nosec B607: fixed external tool "git" with no portable absolute path ["git", "ls-tree", "--name-only", "-r", addons_branch], cwd=grass_addons_dir, stdout=subprocess.PIPE,