Skip to content

Commit

Permalink
Implement flake8-bandit rule S103 (#1636)
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Jan 4, 2023
1 parent 8b8e6e4 commit 1817f87
Show file tree
Hide file tree
Showing 14 changed files with 346 additions and 49 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -763,6 +763,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

0 comments on commit 1817f87

Please sign in to comment.