Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement flake8-bandit rule S103 #1636

Merged
merged 2 commits into from Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -762,6 +762,7 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
| ---- | ---- | ------- | --- |
| S101 | AssertUsed | Use of `assert` detected | |
| S102 | ExecUsed | Use of `exec` detected | |
| S103 | BadFilePermissions | `os.chmod` setting a permissive mask `0o777` on file or directory | |
| S104 | HardcodedBindAllInterfaces | Possible binding to all interfaces | |
| S105 | HardcodedPasswordString | Possible hardcoded password: `"..."` | |
| S106 | HardcodedPasswordFuncArg | Possible hardcoded password: `"..."` | |
Expand Down
22 changes: 22 additions & 0 deletions resources/test/fixtures/flake8_bandit/S103.py
@@ -0,0 +1,22 @@
import os
import stat

keyfile = "foo"

os.chmod("/etc/passwd", 0o227) # Error
os.chmod("/etc/passwd", 0o7) # Error
os.chmod("/etc/passwd", 0o664) # OK
os.chmod("/etc/passwd", 0o777) # Error
os.chmod("/etc/passwd", 0o770) # Error
os.chmod("/etc/passwd", 0o776) # Error
os.chmod("/etc/passwd", 0o760) # OK
os.chmod("~/.bashrc", 511) # Error
os.chmod("/etc/hosts", 0o777) # Error
os.chmod("/tmp/oh_hai", 0x1FF) # Error
os.chmod("/etc/passwd", stat.S_IRWXU) # OK
os.chmod(keyfile, 0o777) # Error
os.chmod(keyfile, 0o7 | 0o70 | 0o700) # Error
os.chmod(keyfile, stat.S_IRWXO | stat.S_IRWXG | stat.S_IRWXU) # Error
os.chmod("~/hidden_exec", stat.S_IXGRP) # Error
os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
os.chmod("/etc/passwd", stat.S_IWOTH) # Error
1 change: 1 addition & 0 deletions ruff.schema.json
Expand Up @@ -889,6 +889,7 @@
"S10",
"S101",
"S102",
"S103",
"S104",
"S105",
"S106",
Expand Down
47 changes: 47 additions & 0 deletions src/ast/helpers.rs
Expand Up @@ -574,6 +574,53 @@ pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator
match_trailing_content(stmt, locator)
}

#[derive(Default)]
/// A simple representation of a call's positional and keyword arguments.
pub struct SimpleCallArgs<'a> {
pub args: Vec<&'a Expr>,
pub kwargs: FxHashMap<&'a str, &'a Expr>,
}

impl<'a> SimpleCallArgs<'a> {
pub fn new(args: &'a Vec<Expr>, keywords: &'a Vec<Keyword>) -> Self {
let mut result = SimpleCallArgs::default();

for arg in args {
match &arg.node {
ExprKind::Starred { .. } => {
break;
}
_ => {
result.args.push(arg);
}
}
}

for keyword in keywords {
if let Some(arg) = &keyword.node.arg {
result.kwargs.insert(arg, &keyword.node.value);
}
}

result
}

/// Get the argument with the given name or position.
/// If the argument is not found with either name or position, return
/// `None`.
pub fn get_argument(&self, name: &'a str, position: Option<usize>) -> Option<&'a Expr> {
if let Some(kwarg) = self.kwargs.get(name) {
return Some(kwarg);
}
if let Some(position) = position {
if position < self.args.len() {
return Some(self.args[position]);
}
}
None
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
Expand Down
11 changes: 11 additions & 0 deletions src/checkers/ast.rs
Expand Up @@ -1868,6 +1868,17 @@ where
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::S103) {
if let Some(check) = flake8_bandit::plugins::bad_file_permissions(
func,
args,
keywords,
&self.from_imports,
&self.import_aliases,
) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::S106) {
self.add_checks(
flake8_bandit::plugins::hardcoded_password_func_arg(keywords).into_iter(),
Expand Down
2 changes: 1 addition & 1 deletion src/flake8_bandit/mod.rs
Expand Up @@ -3,7 +3,6 @@ pub mod plugins;

#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;

use anyhow::Result;
Expand All @@ -15,6 +14,7 @@ mod tests {

#[test_case(CheckCode::S101, Path::new("S101.py"); "S101")]
#[test_case(CheckCode::S102, Path::new("S102.py"); "S102")]
#[test_case(CheckCode::S103, Path::new("S103.py"); "S103")]
#[test_case(CheckCode::S104, Path::new("S104.py"); "S104")]
#[test_case(CheckCode::S105, Path::new("S105.py"); "S105")]
#[test_case(CheckCode::S106, Path::new("S106.py"); "S106")]
Expand Down
108 changes: 108 additions & 0 deletions src/flake8_bandit/plugins/bad_file_permissions.rs
@@ -0,0 +1,108 @@
use num_traits::ToPrimitive;
use once_cell::sync::Lazy;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Operator};

use crate::ast::helpers::{compose_call_path, match_module_member, SimpleCallArgs};
use crate::ast::types::Range;
use crate::registry::{Check, CheckKind};

const WRITE_WORLD: u16 = 0o2;
const EXECUTE_GROUP: u16 = 0o10;

static PYSTAT_MAPPING: Lazy<FxHashMap<&'static str, u16>> = Lazy::new(|| {
FxHashMap::from_iter([
("stat.ST_MODE", 0o0),
("stat.S_IFDOOR", 0o0),
("stat.S_IFPORT", 0o0),
("stat.ST_INO", 0o1),
("stat.S_IXOTH", 0o1),
("stat.UF_NODUMP", 0o1),
("stat.ST_DEV", 0o2),
("stat.S_IWOTH", 0o2),
("stat.UF_IMMUTABLE", 0o2),
("stat.ST_NLINK", 0o3),
("stat.ST_UID", 0o4),
("stat.S_IROTH", 0o4),
("stat.UF_APPEND", 0o4),
("stat.ST_GID", 0o5),
("stat.ST_SIZE", 0o6),
("stat.ST_ATIME", 0o7),
("stat.S_IRWXO", 0o7),
("stat.ST_MTIME", 0o10),
("stat.S_IXGRP", 0o10),
("stat.UF_OPAQUE", 0o10),
("stat.ST_CTIME", 0o11),
("stat.S_IWGRP", 0o20),
("stat.UF_NOUNLINK", 0o20),
("stat.S_IRGRP", 0o40),
("stat.UF_COMPRESSED", 0o40),
("stat.S_IRWXG", 0o70),
("stat.S_IEXEC", 0o100),
("stat.S_IXUSR", 0o100),
("stat.S_IWRITE", 0o200),
("stat.S_IWUSR", 0o200),
("stat.S_IREAD", 0o400),
("stat.S_IRUSR", 0o400),
("stat.S_IRWXU", 0o700),
("stat.S_ISVTX", 0o1000),
("stat.S_ISGID", 0o2000),
("stat.S_ENFMT", 0o2000),
("stat.S_ISUID", 0o4000),
])
});

fn get_int_value(expr: &Expr) -> Option<u16> {
match &expr.node {
ExprKind::Constant {
value: Constant::Int(value),
..
} => value.to_u16(),
ExprKind::Attribute { .. } => {
if let Some(path) = compose_call_path(expr) {
PYSTAT_MAPPING.get(path.as_str()).copied()
} else {
None
}
}
ExprKind::BinOp { left, op, right } => {
if let (Some(left_value), Some(right_value)) =
(get_int_value(left), get_int_value(right))
{
match op {
Operator::BitAnd => Some(left_value & right_value),
Operator::BitOr => Some(left_value | right_value),
Operator::BitXor => Some(left_value ^ right_value),
_ => None,
}
} else {
None
}
}
_ => None,
}
}

/// S103
pub fn bad_file_permissions(
func: &Expr,
args: &Vec<Expr>,
keywords: &Vec<Keyword>,
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
) -> Option<Check> {
if match_module_member(func, "os", "chmod", from_imports, import_aliases) {
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(mode_arg) = call_args.get_argument("mode", Some(1)) {
if let Some(int_value) = get_int_value(mode_arg) {
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
return Some(Check::new(
CheckKind::BadFilePermissions(int_value),
Range::from_located(mode_arg),
));
}
}
}
}
None
}
2 changes: 2 additions & 0 deletions src/flake8_bandit/plugins/mod.rs
@@ -1,4 +1,5 @@
pub use assert_used::assert_used;
pub use bad_file_permissions::bad_file_permissions;
pub use exec_used::exec_used;
pub use hardcoded_bind_all_interfaces::hardcoded_bind_all_interfaces;
pub use hardcoded_password_default::hardcoded_password_default;
Expand All @@ -8,6 +9,7 @@ pub use hardcoded_password_string::{
};

mod assert_used;
mod bad_file_permissions;
mod exec_used;
mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
Expand Down
@@ -0,0 +1,135 @@
---
source: src/flake8_bandit/mod.rs
expression: checks
---
- kind:
BadFilePermissions: 151
location:
row: 6
column: 24
end_location:
row: 6
column: 29
fix: ~
parent: ~
- kind:
BadFilePermissions: 7
location:
row: 7
column: 24
end_location:
row: 7
column: 27
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 9
column: 24
end_location:
row: 9
column: 29
fix: ~
parent: ~
- kind:
BadFilePermissions: 504
location:
row: 10
column: 24
end_location:
row: 10
column: 29
fix: ~
parent: ~
- kind:
BadFilePermissions: 510
location:
row: 11
column: 24
end_location:
row: 11
column: 29
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 13
column: 22
end_location:
row: 13
column: 25
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 14
column: 23
end_location:
row: 14
column: 28
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 15
column: 24
end_location:
row: 15
column: 29
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 17
column: 18
end_location:
row: 17
column: 23
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 18
column: 18
end_location:
row: 18
column: 36
fix: ~
parent: ~
- kind:
BadFilePermissions: 511
location:
row: 19
column: 18
end_location:
row: 19
column: 60
fix: ~
parent: ~
- kind:
BadFilePermissions: 8
location:
row: 20
column: 26
end_location:
row: 20
column: 38
fix: ~
parent: ~
- kind:
BadFilePermissions: 2
location:
row: 22
column: 24
end_location:
row: 22
column: 36
fix: ~
parent: ~

3 changes: 2 additions & 1 deletion src/flake8_pytest_style/plugins/fail.rs
@@ -1,6 +1,7 @@
use rustpython_ast::{Expr, Keyword};

use super::helpers::{is_empty_or_null_string, is_pytest_fail, SimpleCallArgs};
use super::helpers::{is_empty_or_null_string, is_pytest_fail};
use crate::ast::helpers::SimpleCallArgs;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Check, CheckKind};
Expand Down