codeaudit 0.9.1__tar.gz → 0.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeaudit-0.9.1 → codeaudit-0.9.3}/PKG-INFO +2 -1
- {codeaudit-0.9.1 → codeaudit-0.9.3}/README.md +1 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/CONTRIBUTE.md +2 -2
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/_config.yml +1 -1
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/_toc.yml +1 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/assert_check.md +61 -2
- codeaudit-0.9.3/docs/checks/hash_check.md +25 -0
- codeaudit-0.9.3/docs/checks/zipfile_check.md +28 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/codeauditcommands.md +1 -1
- codeaudit-0.9.3/docs/complexitycheck.md +102 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/help.md +1 -1
- codeaudit-0.9.3/docs/issues.md +100 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/license.md +1 -1
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/whysast.md +2 -2
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/__about__.py +1 -1
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/data/sastchecks.csv +1 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/reporting.py +3 -2
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/security_checks.py +1 -2
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/totals.py +1 -1
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_constructspart2.py +19 -0
- codeaudit-0.9.3/tests/test_totalscheck.py +55 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/allshit.py +7 -0
- codeaudit-0.9.3/tests/validationfiles/complexitycheck.py +22 -0
- codeaudit-0.9.3/tests/validationfiles/correctcounts.py +115 -0
- codeaudit-0.9.3/tests/validationfiles/gzip.py +5 -0
- codeaudit-0.9.1/docs/checks/hash_check.md +0 -18
- codeaudit-0.9.1/docs/checks/zipfile_check.md +0 -12
- codeaudit-0.9.1/docs/complexitycheck.md +0 -28
- {codeaudit-0.9.1 → codeaudit-0.9.3}/.gitignore +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/CONTRIBUTE.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/LICENSE.txt +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/SECURITY.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/CLIcommands.ipynb +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/_static/nocxstyle.css +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/about.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/astlines.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/astlines2.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/base64_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/binding_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/builtinfunctions_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/chmod_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/directorycreation_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/exception_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/httpserver_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/input_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/loggingconf_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/marshal_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/mktemp_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/multiprocessing_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/pickle_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/random_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/shelve_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/shutil_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/subprocess_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/syscalls_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/systemcalls_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/tarfile_extract_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checks/xml_check.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/checksinformation.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/codeauditchecks.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/codeauditoverview.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/directoryscan.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/examples/checks_example.html +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/examples/directoryscan.html +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/examples/filescan.html +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/examples/modulescan.html +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/examples/overview.html +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/features.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/filescan.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/filescan.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/OO.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/ROI_logo.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/YourLogoHere.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/codeauditlogo.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/nocxbanner.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/images/overview_linkaudit.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/intro.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/modulescan.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/overviewplot.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/sponsors.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/userguide.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/docs/warnings.md +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/filescan.png +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/pyproject.toml +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/__init__.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/altairplots.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/checkmodules.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/codeaudit.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/complexitycheck.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/filehelpfunctions.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/htmlhelpfunctions.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/issuevalidations.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/src/codeaudit/simple.css +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/__init__.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/count_lines_file1.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_basicpatterns.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_chmod.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_correctexceptionuse.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_count_commentlines.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_directorycreation.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_directorycreation2.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_hashstrenght.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_modulecheck.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_oschecks.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_random.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/test_standardlibconstructs.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/assert.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/base64.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/chmod_things.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/directorycreation.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/directorycreation2.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/exception.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/file3.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/file_with_warnings.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/hashcheck.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/httpserver.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/inputstatement.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/marshal.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/modulecheck.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/multiprocessing.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/oschecks.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/pickle.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/python2_file_willnotwork.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/random.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/shelve.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/shutil.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/subprocess.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/syslibrary.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/tarfilevalidation.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/tempcheck.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/validation1.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/validation2.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/xml.py +0 -0
- {codeaudit-0.9.1 → codeaudit-0.9.3}/tests/validationfiles/zipfile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeaudit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Simplified static security checks for Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/nocomplexity/codeaudit#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/nocomplexity/codeaudit/issues
|
|
@@ -31,6 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
[](https://pypi.org/project/codeaudit)
|
|
32
32
|
[](https://pypi.org/project/codeaudit)
|
|
33
33
|
[](https://www.bestpractices.dev/projects/10970)
|
|
34
|
+
[](https://pepy.tech/projects/codeaudit)
|
|
34
35
|
|
|
35
36
|
Python Codeaudit - A modern Python source code analyzer based on distrust.
|
|
36
37
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/codeaudit)
|
|
6
6
|
[](https://pypi.org/project/codeaudit)
|
|
7
7
|
[](https://www.bestpractices.dev/projects/10970)
|
|
8
|
+
[](https://pepy.tech/projects/codeaudit)
|
|
8
9
|
|
|
9
10
|
Python Codeaudit - A modern Python source code analyzer based on distrust.
|
|
10
11
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Contribute
|
|
2
2
|
|
|
3
|
-
Great that you
|
|
3
|
+
Great that you want to contribute!
|
|
4
4
|
|
|
5
5
|
:::{tip}
|
|
6
6
|
All contributions are welcome!
|
|
7
7
|
Think of corrections on the manual, code and more or better tests.
|
|
8
8
|
:::
|
|
9
9
|
|
|
10
|
-
The **Codeaudit** code repository is hosted at [Github](github.com/nocomplexity/codeaudit).
|
|
10
|
+
The **Codeaudit** code repository is hosted at [Github](https://github.com/nocomplexity/codeaudit).
|
|
11
11
|
|
|
12
12
|
Simple Guidelines:
|
|
13
13
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Book settings
|
|
7
7
|
#title : SimplifiedNLP Documentation # The title of the book. Will be placed in the left navbar.
|
|
8
8
|
title : ""
|
|
9
|
-
author : '<a href="https://nocomplexity.com/">Maikel Mardjan (nocomplexity.com)</a>' # The author of the book
|
|
9
|
+
author : '<a href="https://nocomplexity.com/">Maikel Mardjan (nocomplexity.com)</a> and all contributors.' # The author of the book
|
|
10
10
|
copyright : '2025- Maikel Mardjan - Business Management Support Foundation' # Copyright year to be placed in the footer
|
|
11
11
|
logo : "images/nocxbanner.png" # A path to the book logo
|
|
12
12
|
# Patterns to skip when building the book. Can be glob-style (e.g. "*skip.ipynb")
|
|
@@ -11,7 +11,11 @@ Using `assert` can be problematic from a security perspective!
|
|
|
11
11
|
|
|
12
12
|
1. Assertions are primarily for debugging and development, **NOT** for production validation or error handling.
|
|
13
13
|
|
|
14
|
-
* **They can be disabled:** When Python is run in optimized mode (with the
|
|
14
|
+
* **They can be disabled:** When Python is run in optimized mode (with the `python -O` or `python -OO` flags, or by setting the `PYTHONOPTIMIZE` environment variable), `assert` statements are completely ignored.
|
|
15
|
+
|
|
16
|
+
When using `python -O` or `python -OO` the Python interpreter removes all assert statements from the bytecode.
|
|
17
|
+
|
|
18
|
+
This means any crucial checks you rely on for security or data integrity will simply vanish, leaving your application vulnerable.
|
|
15
19
|
|
|
16
20
|
* **Not for user input validation:** So never use `assert` to validate user input or external data. If assertions are disabled in production, malicious or malformed input will bypass your checks, potentially leading to crashes, data corruption, or even arbitrary code execution. Use `if/else` statements with proper exception handling (e.g., `ValueError`, `TypeError`) for this.
|
|
17
21
|
|
|
@@ -31,10 +35,65 @@ Using `assert` can be problematic from a security perspective!
|
|
|
31
35
|
`assert` statements **SHOULD** be removed when not running in debug mode (i.e. when invoking the Python command with the -O or -OO options).
|
|
32
36
|
|
|
33
37
|
|
|
38
|
+
## Example
|
|
39
|
+
|
|
40
|
+
The following Python example shows how trusting on `assert` can lead to another behaviour.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
"""Example of Python script using the assert statement - which can lead to security issues!"""
|
|
45
|
+
|
|
46
|
+
def divide_numbers(x,y):
|
|
47
|
+
"""
|
|
48
|
+
Divides two numbers.
|
|
49
|
+
Uses an assert statement to ensure the denominator is not zero.
|
|
50
|
+
"""
|
|
51
|
+
# Assert that the denominator is not zero.
|
|
52
|
+
# If denominator is 0, an AssertionError will be raised with the given message.
|
|
53
|
+
assert y != 2, "Error: diving is crying!"
|
|
54
|
+
return x / y
|
|
55
|
+
|
|
56
|
+
print("--- Demonstrating danger of assertions ---")
|
|
57
|
+
result = divide_numbers(10, 3)
|
|
58
|
+
print(f"Dividing result : {result}")
|
|
59
|
+
|
|
60
|
+
result2 = 'Error- Dividing is crying!' # A default value to show
|
|
61
|
+
try:
|
|
62
|
+
# If run with 'python -O', the 'assert y != 0' line above will be removed.
|
|
63
|
+
# In that scenario, this call will directly raise a ZeroDivisionError,
|
|
64
|
+
# as the assert check won't be present.
|
|
65
|
+
result2 = divide_numbers(10, 2)
|
|
66
|
+
except AssertionError as e:
|
|
67
|
+
# This block will be executed if assert is active (without -O)
|
|
68
|
+
print(f"Caught an AssertionError: {e}")
|
|
69
|
+
print('Never divide! Take it all or divide in more parts.')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
print(f"Dividing result - Result should be error, not 5! - : {result2}")
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run this program with:
|
|
77
|
+
```
|
|
78
|
+
python assert_example.py
|
|
79
|
+
```
|
|
80
|
+
And after that another time with:
|
|
81
|
+
```
|
|
82
|
+
python -O assert_example.py
|
|
83
|
+
```
|
|
84
|
+
And notice the different outcome.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
So this examples shows that if you rely on `assert` to prevent an `AssertionError` (or any other critical condition), that check will simply disappear in an optimized environment!
|
|
88
|
+
|
|
89
|
+
Instead of catching an `AssertionError`, a program will run differently when `python -O` is used. And this can and will have consequences for the functional working of a program. But worse it can have severe security consequences. E.g. if asserts are used to validate user input to prevent sql injections e.g.
|
|
90
|
+
|
|
91
|
+
For robust validation and error handling in production code, always use standard if statements combined with raising appropriate exceptions (like `ValueError`, `TypeError`, or custom exceptions) rather than assert.
|
|
92
|
+
|
|
34
93
|
|
|
35
94
|
|
|
36
95
|
## More information
|
|
37
96
|
|
|
38
97
|
* [The assert statement - Python Documentation](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
|
|
39
98
|
* [The dangers of assert in Python](https://snyk.io/blog/the-dangers-of-assert-in-python/)
|
|
40
|
-
* [Feature: Python assert should be consider harmful](https://community.sonarsource.com/t/feature-python-assert-should-be-consider-harmful/38501) But note that Sonar did not implement this check.
|
|
99
|
+
* [Feature: Python assert should be consider harmful](https://community.sonarsource.com/t/feature-python-assert-should-be-consider-harmful/38501) But note that Sonar did not implement this check.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Insecure hashing
|
|
2
|
+
|
|
3
|
+
Codeaudit checks the use of insecure hashing functions.
|
|
4
|
+
|
|
5
|
+
The Python library `hashlib` is great. But using insecure hashing algorithms is still possible and should be avoided!
|
|
6
|
+
|
|
7
|
+
So CodeAudit performs a check on usage of the insecure hash algorithms:
|
|
8
|
+
* md5
|
|
9
|
+
* sha1
|
|
10
|
+
|
|
11
|
+
[From Python 3.9 and higher](https://docs.python.org/3/library/hashlib.html#hashlib-usedforsecurity):
|
|
12
|
+
* All hashlib constructors take a keyword-only argument `usedforsecurity` with default value True. A false value allows the use of insecure and blocked hashing algorithms in restricted environments. False indicates that the hashing algorithm is not used in a security context, e.g. as a non-cryptographic one-way compression function.
|
|
13
|
+
|
|
14
|
+
:::{danger}
|
|
15
|
+
Unless there is a very good reason to still use `md5` or `sha1`, which is almost impossible, you should demand a fix. Or if you are the developer of the code make the fix.
|
|
16
|
+
:::
|
|
17
|
+
|
|
18
|
+
## More information
|
|
19
|
+
|
|
20
|
+
* https://docs.python.org/3/library/hashlib.html#hashlib-usedforsecurity
|
|
21
|
+
* [Attacks on cryptographic hash algorithms](https://en.wikipedia.org/wiki/Cryptographic_hash_function#Attacks_on_cryptographic_hash_algorithms)
|
|
22
|
+
* https://cwe.mitre.org/data/definitions/327.html
|
|
23
|
+
* [OWASP Top 10:2021 ](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
|
|
24
|
+
* [CWE-327: Use of a Broken or Risky Cryptographic Algorithm](https://cwe.mitre.org/data/definitions/327.html)
|
|
25
|
+
* [CWE-328: Use of Weak Hash](https://cwe.mitre.org/data/definitions/328.html)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Check on zipfiles extraction
|
|
2
|
+
|
|
3
|
+
When using the Python module `zipfile` there is a risk processing maliciously prepared `.zip files`. This can availability issues due to storage exhaustion.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Validations are done on `zipfile` methods:
|
|
7
|
+
* `.extractall`
|
|
8
|
+
* `.open` and more.
|
|
9
|
+
|
|
10
|
+
And `gzip` methods:
|
|
11
|
+
* `gzip.open`
|
|
12
|
+
|
|
13
|
+
## Gzip potential danger
|
|
14
|
+
When using `gzip.open` the potential security issue is related to resource consumption if the file is untrusted.
|
|
15
|
+
|
|
16
|
+
This can lead to:
|
|
17
|
+
* **Denial of Service via Resource Exhaustion**
|
|
18
|
+
If a gzip file is controlled by a malicious user, they could create a highly compressed file that expands to an enormous size when decompressed. This is known as a "zip bomb."
|
|
19
|
+
|
|
20
|
+
Such `gzip` file could quickly consume all of the system's available RAM, causing the application to crash or the server to become unresponsive. This is a common attack vector when processing user-uploaded or external compressed files.
|
|
21
|
+
|
|
22
|
+
* **Potential Path Traversal**
|
|
23
|
+
A path traversal vulnerability could arise if the file in the `gzip` file is constructed from user input. For example, if the path came from a web request, a user could provide a path like ../../../../etc/passwd.gz to access sensitive files outside of the intended directory. This is a critical security consideration for any code that handles file paths based on external data that is decompressed with `gzip.open`.
|
|
24
|
+
|
|
25
|
+
## More information
|
|
26
|
+
|
|
27
|
+
* https://docs.python.org/3/library/zipfile.html#zipfile-resources-limitations
|
|
28
|
+
* https://docs.python.org/3/library/gzip.html
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Codeaudit complexity Check
|
|
2
|
+
|
|
3
|
+
The Python `codeaudit` tool implements a Simple Cyclomatic complexity check.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[Cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) is a software metric used to indicate the complexity of a program. It was developed by Thomas J. McCabe, Sr. in 1976.
|
|
7
|
+
|
|
8
|
+
Calculating the Cyclomatic complexity for Python sources is complex to do right. And seldom needed! Most implementations for calculating a very thorough Cyclomatic Complexity end up being opinionated sooner or later.
|
|
9
|
+
|
|
10
|
+
:::{note}
|
|
11
|
+
Codeaudit takes a pragmatic and simple approach to determine and calculate the complexity of a source file.
|
|
12
|
+
|
|
13
|
+
**BUT:**
|
|
14
|
+
The Complexity Score that Codeaudit presents gives a **good and solid** representation for the complexity of a Python source file.
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
But I known the complexity score is not an exact exhaustive cyclomatic complexity measurement.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
The complexity is determined per file, and not per function within a Python source file. I have worked long ago with companies that calculated [function points](https://en.wikipedia.org/wiki/Function_point) for software that needed to be created or adjusted. Truth is: Calculating exact metrics about complexity for software code projects is a lot of work, is seldom done correctly and are seldom used with nowadays devops or scrum development teams.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
:::{tip}
|
|
25
|
+
The complexity score of source code gives presented gives a solid indication from a security perspective.
|
|
26
|
+
:::
|
|
27
|
+
|
|
28
|
+
Complex code has a lot of disadvantages when it comes to managing security risks. Making corrections is difficult and errors are easily made.
|
|
29
|
+
|
|
30
|
+
## What is reported
|
|
31
|
+
|
|
32
|
+
Python Code Audit overview displays:
|
|
33
|
+
* `Median_Complexity` (middle value) as score in an overview report (`codeaudit overview`) for all files of a package or a directory.
|
|
34
|
+
* `Maximum_Complexity` as score in an overview report (`codeaudit overview`). This to see in one appearance if a file that **SHOULD** require a closer look from a security perspective is present.
|
|
35
|
+
|
|
36
|
+
## How is Complexity determined
|
|
37
|
+
|
|
38
|
+
Python Code Audit calculates the cyclomatic complexity of Python code using Python’s built-in `ast` (Abstract Syntax Tree) module.
|
|
39
|
+
|
|
40
|
+
## What is Cyclomatic Complexity?
|
|
41
|
+
|
|
42
|
+
From a security perspective the having an objective number for Python code complexity is crucial. Complex code has another risks profile from a security perspective. Think of cost, validations needed, expertise needed and so on. Complex code is known to be more vulnerable.
|
|
43
|
+
|
|
44
|
+
:::{admonition} Definition
|
|
45
|
+
:class: note
|
|
46
|
+
Cyclomatic complexity is a software metric used to measure the number of independent paths through a program's source code. More paths mean more logic branches and greater potential for bugs, testing effort, or maintenance complexity.
|
|
47
|
+
:::
|
|
48
|
+
|
|
49
|
+
## How does Code Audit calculates the Complexity?
|
|
50
|
+
|
|
51
|
+
Every function, method, or script starts with a base complexity of 1 (i.e., one execution path with no branching).
|
|
52
|
+
|
|
53
|
+
It adds 1 for each control structure or branching point:
|
|
54
|
+
| **AST Node Type** | **Reason for Increasing Complexity** |
|
|
55
|
+
|---|---|
|
|
56
|
+
| If | Conditional branch (if/elif/else) |
|
|
57
|
+
| For, While | Loop constructs (create additional paths) |
|
|
58
|
+
| Try | Potential for exception handling (adds branch) |
|
|
59
|
+
| ExceptHandler | Each except adds a new error-handling path |
|
|
60
|
+
| With | Context manager entry/exit paths |
|
|
61
|
+
| BoolOp | and / or are logical branches |
|
|
62
|
+
| Match | Match statement (like switch in other langs) |
|
|
63
|
+
| MatchCase | Each case adds an alternative path |
|
|
64
|
+
| Assert | Introduces an exit point if the condition fails |
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
```
|
|
69
|
+
"""complexity of code below should count to 4
|
|
70
|
+
Complexity breakdown:
|
|
71
|
+
1 (base)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
+1 (if)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
+1 (and) operator inside if
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
+1 (elif) — counted as another If node in AST
|
|
81
|
+
= 4
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
def test(x):
|
|
86
|
+
if x > 0 and x < 10:
|
|
87
|
+
print("Single digit positive")
|
|
88
|
+
elif x >= 10:
|
|
89
|
+
print("Two digits or more")
|
|
90
|
+
else:
|
|
91
|
+
print("Non-positive")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You can verify the complexity of any Python file the command:
|
|
95
|
+
```
|
|
96
|
+
codeaudit filescan <filename.py>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Summary:
|
|
100
|
+
* Python Code Audit only analyzes the top-level structure. It doesn't distinguish between functions on purpose, unless the input is separated accordingly.
|
|
101
|
+
* Python Code Audit uses a simplified cyclomatic complexity approach to get fast inside from a security perspective. This may differ from tools, especially since implementation choices that are made for dealing with comprehensions, nested functions will be different.
|
|
102
|
+
|
|
@@ -16,7 +16,7 @@ Helping is possible in multiple ways:
|
|
|
16
16
|
* [Support this work](sponsors)
|
|
17
17
|
* Share and promote the use of this solid simple tool
|
|
18
18
|
* Send me a line that you use this tool within your company.
|
|
19
|
-
* [Contribute](
|
|
19
|
+
* [Contribute](CONTRIBUTE)
|
|
20
20
|
|
|
21
21
|
Guideline to contribute:
|
|
22
22
|
* For questions, feature requests and Bug Reports please use on the [Github Issue Tracker](https://github.com/nocomplexity/codeaudit/issues).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Mitigating security issues
|
|
2
|
+
|
|
3
|
+
Python Code Audit scans and checks for **potential security issues**. A potential security issue is a weakness that **can** lead to a security vulnerability with impact.
|
|
4
|
+
|
|
5
|
+
There is an important difference between a **potential security issue** and a **security vulnerability** in Python code:
|
|
6
|
+
|
|
7
|
+
:::{important}
|
|
8
|
+
A **potential security issue** or weakness is a general flaw, error, mistake or sloppy programming habit in a programs design, implementation, or operation that could lead to security problems. It's a potential area of concern that might not be immediately exploitable but increases the risk of a vulnerability emerging.
|
|
9
|
+
|
|
10
|
+
Depending on the **context** where a Python program is executed, found security issues should be fixed, or can be neglected.
|
|
11
|
+
|
|
12
|
+
:::
|
|
13
|
+
|
|
14
|
+
Examples of **potential security issues** or weaknesses that Python Code Audit discovers:
|
|
15
|
+
* Risks of running untrusted code by using Python statements that allow this. Think of `compile`, `eval` or `exec`.
|
|
16
|
+
* Availability risks. Operating systems functions that can create directories or files can cause availability risks.
|
|
17
|
+
* Risks of changing permission on files or directories. Python files are too often run with too broad permissions. This can lead to severe risks on data leakage or integrity issues on files.
|
|
18
|
+
|
|
19
|
+
See [section `command codeaudit checks`](codeauditchecks) to get more insight on implemented security validations.
|
|
20
|
+
|
|
21
|
+
Issues in Python code are not necessarily directly exploitable on their own, but detected security issues are a fertile ground for vulnerabilities to appear.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
A vulnerability is an exploitable weakness. So minimize the risks for vulnerabilities and take the reported **potential security issues** serious.
|
|
26
|
+
|
|
27
|
+
:::{danger}
|
|
28
|
+
If a weakness in Python code exists, and **if** a method is found to take advantage of that weakness to cause harm, then it becomes a vulnerability.
|
|
29
|
+
|
|
30
|
+
Addressing weaknesses [proactively](https://nocomplexity.com/documents/simplifysecurity/shiftleft.html#shift-left) helps prevent vulnerabilities from emerging, while patching vulnerabilities reactively addresses known exploitable flaws.
|
|
31
|
+
:::
|
|
32
|
+
|
|
33
|
+
## Dealing with false positives
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
:::{note}
|
|
37
|
+
Python Code Audit only reports **potential security issues**.
|
|
38
|
+
It is up to the user, developer, security tester or someone with the required **security** and **Python** knowledge to decide if action is needed.
|
|
39
|
+
|
|
40
|
+
This Python Code Audit SAST tool, but this accounts for all SAST tools, analyze code in isolation. So without the runtime environment or how the any knowledge on how code interacts with other components and who the users are.
|
|
41
|
+
|
|
42
|
+
For good security you always need to construct a [threat model](https://nocomplexity.com/documents/securityarchitecture/architecture/threadmodels.html#threat-models) in able to evaluate if potential issues should be resolved.
|
|
43
|
+
|
|
44
|
+
Despite the rise of AI, no single tool can judge the **context** where a program is used, how it is used and by whom. So if you are in doubt if action on a reported issues is needed, you **SHOULD** contact a security specialist who has deep technical knowledge of cyber security also solid knowledge on developing secure Python programs. In our [section sponsors](sponsors) you find agencies or consultants that can provide the needed assistance. Do not hesitate to get professional help, cyber security is a complex area!
|
|
45
|
+
:::
|
|
46
|
+
|
|
47
|
+
:::{warning}
|
|
48
|
+
DO **NOT** rely on SAST scanners that are powered by AI-agents / LLM systems to solve your cyber security problems!
|
|
49
|
+
|
|
50
|
+
Most are just far from good enough.
|
|
51
|
+
|
|
52
|
+
In the best case scenario, you'll only be disappointed. But the risk of a false sense of security is enormous.
|
|
53
|
+
:::
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
:::{note}
|
|
59
|
+
The static nature of the Python Code Audit SAST scan is that it can’t identify vulnerabilities, but only reports **potential security issues**
|
|
60
|
+
:::
|
|
61
|
+
|
|
62
|
+
Python Code Audit is designed to minimize so called 'false positives`.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
A false positive arises when a static analysis tool falsely claims that a construct in the code is insecure. Python Code Audit has no false positives, it will only reports on **potential security issues**.
|
|
66
|
+
|
|
67
|
+
Python Code Audit is created for:
|
|
68
|
+
* Security researchers
|
|
69
|
+
* Users and
|
|
70
|
+
* Developers
|
|
71
|
+
Every user group has its own requirements and needs. And will review the found issues from a different perspective and context.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Python Code Audit will **not** detect and report imported modules that are commented out. E.g. in the following file:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
"""Sample file for module check"""
|
|
78
|
+
|
|
79
|
+
import linkaudit #has no data in OSV DB
|
|
80
|
+
import pandas #has some minor data in OSV
|
|
81
|
+
import requests #has lots of OSV data
|
|
82
|
+
#import numpy #has lots of OSV data! (a lot!!)
|
|
83
|
+
|
|
84
|
+
import os
|
|
85
|
+
import random
|
|
86
|
+
import csv
|
|
87
|
+
|
|
88
|
+
def donothing():
|
|
89
|
+
print('no way!')
|
|
90
|
+
os.chmod('ooooooooooooono.txt',0x777) #this will give an alert on codeaudit filescan!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `numpy` module is not reported on a module check and is not counted in the statistics that Python Code Audit created. This because this module is commented out. Modules that are imported, but not yet used are reported. This because it is likely that the modules will be used in a next iteration of the Python file. And checking as fast as possible on possible security issues for modules that *could* be used makes sense.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
:::{note}
|
|
98
|
+
Python Code Audit architecture, design , implementation overcome the shortcoming of many 'legacy' SAST tools who are good at reporting false positives, but lack good validation for correct secure Python use.
|
|
99
|
+
|
|
100
|
+
:::
|
|
@@ -55,7 +55,7 @@ material.
|
|
|
55
55
|
## Software License
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
Codeaudit is a Python program to check potential security issues in Python files.
|
|
58
|
+
Codeaudit is a Python program to check for potential security issues in Python files.
|
|
59
59
|
|
|
60
60
|
Copyright (C) 2025 BM-Support.org Foundation and Maikel Mardjan.
|
|
61
61
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Why Security
|
|
1
|
+
# Why Security Testing
|
|
2
2
|
|
|
3
3
|
:::{note}
|
|
4
4
|
Static application security testing(SAST) for python source code is a MUST!
|
|
@@ -8,7 +8,7 @@ Static application security testing(SAST) for python source code is a MUST!
|
|
|
8
8
|
:::
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
Python is
|
|
11
|
+
Python is one of the most used programming language to date. Especially in the AI/ML world and the cyber security world, most tools are based on Python programs.
|
|
12
12
|
|
|
13
13
|
Large and small businesses use and trust Python to run their business. Python is from security perspective a **good** choice. However even when using Python the risk on security issues is never zero.
|
|
14
14
|
|
|
@@ -54,6 +54,7 @@ Shelve module,shelve.open,High,Only loading a shelve from a trusted source is se
|
|
|
54
54
|
Multiprocessing ,connection.recv,High,Connection.recv() uses pickle
|
|
55
55
|
Multiprocessing ,multiprocessing.connection.Connection,High,Connection.recv() uses pickle
|
|
56
56
|
Zipfile,zipfile.ZipFile,High,Extracting files within a program should never be trusted by default. This issue is detected when the zipfile and/or tarfile module with an extraction method is used.
|
|
57
|
+
Gzip,gzip.open,Medium,Potential resource consumption if the file is untrusted.
|
|
57
58
|
shutil,shutil.unpack_archive,Medium,Extracting files within a program should not be trusted by default.
|
|
58
59
|
shutil,shutil.copy,Medium,Information can be transfered without permission.
|
|
59
60
|
shutil,shutil.copy2,Medium,Information can be transfered without permission.
|
|
@@ -82,8 +82,9 @@ def overview_report(directory, filename=DEFAULT_OUTPUT_FILE):
|
|
|
82
82
|
html += '</details>'
|
|
83
83
|
html += f'<h2>Detailed overview per source file</h2>'
|
|
84
84
|
html += '<details>'
|
|
85
|
-
html += '<summary>Click to see the report details.</summary>'
|
|
86
|
-
|
|
85
|
+
html += '<summary>Click to see the report details.</summary>'
|
|
86
|
+
df_plot = pd.DataFrame(result) # again make the df from the result variable
|
|
87
|
+
html += df_plot.to_html(escape=True,index=False)
|
|
87
88
|
html += '</details>'
|
|
88
89
|
# I now want only a plot for LoC, so drop other columns from Dataframe
|
|
89
90
|
df_plot = pd.DataFrame(result) # again make the df from the result variable
|
|
@@ -40,8 +40,7 @@ def ast_security_checks():
|
|
|
40
40
|
|
|
41
41
|
def perform_validations(sourcefile):
|
|
42
42
|
"""For now a list defined here in this file"""
|
|
43
|
-
checks = ast_security_checks()
|
|
44
|
-
#df = pd.DataFrame(security_validations)
|
|
43
|
+
checks = ast_security_checks()
|
|
45
44
|
constructs = checks['construct'].to_list()
|
|
46
45
|
|
|
47
46
|
source = read_in_source_file(sourcefile)
|
|
@@ -155,7 +155,7 @@ def overview_per_file(python_file):
|
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def overview_count(df):
|
|
158
|
-
"""returns a dataframe with simple overview
|
|
158
|
+
"""returns a dataframe with simple overview for all files"""
|
|
159
159
|
columns_to_sum = [
|
|
160
160
|
"Number_Of_Lines",
|
|
161
161
|
"AST_Nodes",
|
|
@@ -150,5 +150,24 @@ def test_tempfile_incorrect_use():
|
|
|
150
150
|
expected_data = {'tempfile.mktemp': [3]}
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
# Assert that the actual data matches the expected data
|
|
154
|
+
assert actual_data == expected_data
|
|
155
|
+
|
|
156
|
+
def test_gzip_use():
|
|
157
|
+
current_file_directory = Path(__file__).parent
|
|
158
|
+
|
|
159
|
+
# validation1.py is in a subfolder:
|
|
160
|
+
validation_file_path = current_file_directory / "validationfiles" / "gzip.py"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
result = perform_validations(validation_file_path)
|
|
164
|
+
|
|
165
|
+
#actual_data = find_constructs(source, constructs)
|
|
166
|
+
actual_data = result['result']
|
|
167
|
+
|
|
168
|
+
# This is the expected dictionary
|
|
169
|
+
expected_data = {'gzip.open': [4]}
|
|
170
|
+
|
|
171
|
+
|
|
153
172
|
# Assert that the actual data matches the expected data
|
|
154
173
|
assert actual_data == expected_data
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codeaudit.filehelpfunctions import read_in_source_file
|
|
5
|
+
|
|
6
|
+
from codeaudit.totals import read_in_source_file , overview_per_file , count_ast_objects
|
|
7
|
+
|
|
8
|
+
def test_overview_per_file_check():
|
|
9
|
+
current_file_directory = Path(__file__).parent
|
|
10
|
+
# validation1.py is in a subfolder:
|
|
11
|
+
validation_file_path = current_file_directory / "validationfiles" / "correctcounts.py"
|
|
12
|
+
|
|
13
|
+
# source = read_in_source_file(validation_file_path)
|
|
14
|
+
|
|
15
|
+
actual_data = overview_per_file(validation_file_path)
|
|
16
|
+
actual_data.pop('FilePath', None)
|
|
17
|
+
|
|
18
|
+
# This is the expected dictionary
|
|
19
|
+
expected_data = {
|
|
20
|
+
"FileName": "correctcounts.py",
|
|
21
|
+
"Number_Of_Lines": 115,
|
|
22
|
+
"AST_Nodes": 57,
|
|
23
|
+
"Std-Modules": 2,
|
|
24
|
+
"External-Modules": 2,
|
|
25
|
+
"Functions": 6,
|
|
26
|
+
"Classes": 0,
|
|
27
|
+
"Comment_Lines": 24,
|
|
28
|
+
"Complexity_Score": 9,
|
|
29
|
+
"warnings": 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Assert that the actual data matches the expected data
|
|
33
|
+
assert actual_data == expected_data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_count_ast_objects():
|
|
37
|
+
current_file_directory = Path(__file__).parent
|
|
38
|
+
# validation1.py is in a subfolder:
|
|
39
|
+
validation_file_path = current_file_directory / "validationfiles" / "correctcounts.py"
|
|
40
|
+
|
|
41
|
+
source = read_in_source_file(validation_file_path)
|
|
42
|
+
|
|
43
|
+
actual_data = count_ast_objects(source)
|
|
44
|
+
|
|
45
|
+
# This is the expected dictionary
|
|
46
|
+
expected_data = {
|
|
47
|
+
"AST_Nodes": 57,
|
|
48
|
+
"Std-Modules": 2,
|
|
49
|
+
"External-Modules": 2,
|
|
50
|
+
"Functions": 6,
|
|
51
|
+
"Classes": 0,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Assert that the actual data matches the expected data
|
|
55
|
+
assert actual_data == expected_data
|
|
@@ -420,3 +420,10 @@ def run_with_trace2():
|
|
|
420
420
|
import tempfile
|
|
421
421
|
|
|
422
422
|
temp_filename = tempfile.mktemp()
|
|
423
|
+
|
|
424
|
+
"""Checking on gzip.open"""
|
|
425
|
+
import gzip as untrusteddanger
|
|
426
|
+
|
|
427
|
+
content = b"Lots of content here"
|
|
428
|
+
with untrusteddanger.open('/home/joe/file.txt.gz', 'wb') as f:
|
|
429
|
+
f.write(content)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""complexity of code below should count to 4
|
|
2
|
+
Complexity breakdown:
|
|
3
|
+
1 (base)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
+1 (if)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
+1 (and) operator inside if
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
+1 (elif) — counted as another If node in AST
|
|
13
|
+
= 4
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
def test(x):
|
|
17
|
+
if x > 0 and x < 10:
|
|
18
|
+
print("Single digit positive")
|
|
19
|
+
elif x >= 10:
|
|
20
|
+
print("Two digits or more")
|
|
21
|
+
else:
|
|
22
|
+
print("Non-positive")
|