Skip to content

Commit 65b9cfb

Browse files
committed
added extra unit tests for filehelpfunctions
1 parent 397bac2 commit 65b9cfb

3 files changed

Lines changed: 288 additions & 0 deletions

File tree

docs/validatetips.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Checklist for Python Security Applications
2+
3+
Use this checklist to carry out a risk assessment before adopting any security application developed in Python. It is designed to help identify potential weaknesses, evaluate code quality, and ensure the project meets minimal essential security standards. Applying this checklist consistently will support informed decision-making and reduce the risk of introducing security weaknesses into your environment.
4+
5+
6+
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
7+
8+
A security project MUST meet the highest security quality standards. No tool is perfect, and the way a design is converted into a program is always subjective. There is no single "best" way to create and implement a design; there are always multiple options. However, there are fundamentally wrong ways to develop software that cause weaknesses and lead to vulnerabilities. This MUST be avoided in security-critical software. A security program that is relied upon MUST be inherently secure and MUST NOT introduce vulnerabilities.
9+
10+
There are few specific evaluation criteria that can be directly applied to evaluate Python security applications.
11+
Transparency, comprehensive documentation, and the practice of proven security-by-design principles MUST be considered a bare minimum.
12+
13+
While security projects vary, the documentation MUST clearly state when a requirement is not applicable. This prevents ambiguity during security reviews and ensures users that even trivial security aspects have been addressed.
14+
To evaluate or create Python security applications, the following minimal security requirements MUST be met:
15+
16+
1. **Security Policy**
17+
18+
A security project MUST have a security policy, such as a [SECURITY.md](https://github.com/nocomplexity/codeaudit/blob/main/SECURITY.md) file, to ensure that potential security issues can be reported securely and effectively.
19+
20+
+++
21+
22+
2. **OpenSSF Best Practices**
23+
24+
25+
A security project MUST maintain an [OpenSSF Best Practices](https://www.bestpractices.dev/en) badge to demonstrate that all fundamental security requirements have been satisfied.
26+
27+
+++
28+
29+
**3. Fuzzing**
30+
31+
32+
A fuzzer SHOULD be incorporated into the testing workflow where appropriate to identify unexpected behaviours or vulnerabilities.
33+
34+
+++
35+
36+
37+
38+
4. **Architecture and Design**
39+
40+
41+
A formal architecture or design document MUST exist, outlining key principles and documenting the design decisions that guided the implementation. It is RECOMMENDED that a "Security by Design" approach is followed to ensure security principles are integrated into every stage of the SDLC cycle.
42+
A common practice is to maintain an [ARCHITECTURE.md](architecture) file within the repository.
43+
This document SHOULD be released under a Creative Commons licence (or equivalent) and MUST be available without limitation to allow for public review and improvement.
44+
45+
+++
46+
47+
48+
5. **Dependency Validation**
49+
50+
51+
All dependencies SHOULD be validated against known security vulnerabilities.
52+
53+
54+
+++
55+
56+
57+
6. **Dependency Versioning**
58+
59+
60+
The `project.toml` file MUST use exact version identifiers (e.g., `==1.2.3` rather than `>1.2`). This is a critical measure to prevent typosquatting and other supply-chain weaknesses. While [PEP 508](https://peps.python.org/pep-0508/ ) allows logical operators, for the purpose of this security standard, pinning to a specific version is required. Advantage is also that the Python part of the package is bit-for-bit reproducible.
61+
62+
63+
+++
64+
65+
66+
7. **Reproducible Builds**
67+
68+
69+
A package SHOULD be published using Reproducible Builds. It is preferred to use a build tool for PyPI distribution that supports reproducible builds by default.
70+
71+
72+
+++
73+
74+
75+
8. **Principle of Least Privilege**
76+
77+
78+
The program SHOULD require minimal privileges to execute. Administrative or high-privilege accounts MUST NOT be used if they could compromise the system. If specific authorizations are required, a separate service account MUST be created to facilitate clear security logging. The system documentation MUST clearly outline all required authorizations.
79+
80+
81+
+++
82+
83+
84+
9. **Defence in Depth**
85+
86+
The Defence in Depth principle MUST be practised across design, implementation, and testing. For instance, multiple SAST tools SHOULD be used for code validation to ensure comprehensive coverage.
87+
88+
89+
+++
90+
91+
92+
10. **SAST Scanning**
93+
94+
95+
All Python source code MUST be validated using a trusted open-source SAST scanner, such as [Python Code Audit](https://github.com/nocomplexity/codeaudit). Where weaknesses are mitigated but still trigger a notification, they MUST be marked within the code with a clarifying comment.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# SPDX-FileCopyrightText: 2025-present Maikel Mardjan(https://nocomplexity.com/) and all contributors!
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
"""
4+
Validation on correct behaviour of collect_python_source_files function.
5+
6+
"""
7+
import pytest
8+
import os
9+
from pathlib import Path
10+
11+
from codeaudit.filehelpfunctions import collect_python_source_files
12+
13+
def test_underscore_file_inclusion(tmp_path):
14+
"""
15+
Verifies that files starting with underscores (like __init__.py)
16+
are INCLUDED in the current os.walk implementation.
17+
"""
18+
# 1. Setup: Create a mix of files
19+
(tmp_path / "normal.py").write_text("x = 1")
20+
(tmp_path / "_private.py").write_text("x = 2")
21+
(tmp_path / "__init__.py").write_text("# init file")
22+
(tmp_path / ".hidden.py").write_text("x = 3") # Should be skipped
23+
24+
# 2. Action
25+
results = collect_python_source_files(str(tmp_path))
26+
27+
# Convert results to filenames for easy comparison
28+
found_filenames = [os.path.basename(p) for p in results]
29+
30+
# 3. Assertions
31+
assert "normal.py" in found_filenames
32+
assert "_private.py" in found_filenames # This confirms the current Version 2 logic
33+
assert "__init__.py" in found_filenames # This confirms the current Version 2 logic
34+
assert ".hidden.py" not in found_filenames # This confirms dot-files are skipped
35+
36+
def test_collect_skips_invalid_ast(tmp_path, capsys):
37+
# 1. Create one valid file and one invalid file
38+
valid_file = tmp_path / "good.py"
39+
valid_file.write_text("x = 10")
40+
41+
invalid_file = tmp_path / "bad.py"
42+
invalid_file.write_text("this is definitely not python syntax :::")
43+
44+
results = collect_python_source_files(str(tmp_path))
45+
46+
assert any("good.py" in r for r in results)
47+
48+
# The invalid file should NOT be there
49+
assert not any("bad.py" in r for r in results)
50+
51+
# Verify the error message was printed to the console
52+
captured = capsys.readouterr()
53+
assert "skipped due to syntax error" in captured.out
54+
55+
56+
def test_collect_python_files_full_logic(tmp_path):
57+
"""
58+
Tests the three main behaviors of collect_python_source_files:
59+
1. It skips excluded directories (tests, docs, etc.)
60+
2. It skips hidden files (starting with '.')
61+
3. It filters out files that are not AST parsable.
62+
"""
63+
64+
valid_file = tmp_path / "main.py"
65+
valid_file.write_text("def hello(): print('world')", encoding="utf-8")
66+
67+
underscore_file = tmp_path / "__init__.py"
68+
underscore_file.write_text("# package init", encoding="utf-8")
69+
70+
# File in an EXCLUDED directory (Should be skipped)
71+
docs_dir = tmp_path / "docs"
72+
docs_dir.mkdir()
73+
excluded_file = docs_dir / "setup.py"
74+
excluded_file.write_text("print('skip me')", encoding="utf-8")
75+
76+
# Hidden file (Should be skipped by default exclude filter)
77+
hidden_file = tmp_path / ".config.py"
78+
hidden_file.write_text("secret = True", encoding="utf-8")
79+
80+
# Invalid Python file (AST Syntax Error - Should be skipped)
81+
invalid_file = tmp_path / "broken.py"
82+
invalid_file.write_text("if True: \n print('Missing closing parenthesis'", encoding="utf-8")
83+
84+
results = collect_python_source_files(str(tmp_path))
85+
found_filenames = [os.path.basename(p) for p in results]
86+
87+
assert "main.py" in found_filenames
88+
assert "__init__.py" in found_filenames
89+
90+
# These should be filtered out
91+
assert "setup.py" not in found_filenames # Directory excluded
92+
assert ".config.py" not in found_filenames # Hidden file
93+
assert "broken.py" not in found_filenames # AST Parse error
94+
95+
# Ensure we got exactly 2 files
96+
assert len(found_filenames) == 2
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# SPDX-FileCopyrightText: 2025-present Maikel Mardjan(https://nocomplexity.com/) and all contributors!
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import pytest
5+
from pathlib import Path
6+
import sys
7+
8+
from codeaudit.filehelpfunctions import read_in_source_file
9+
10+
11+
def test_read_valid_python_file(tmp_path):
12+
file = tmp_path / "test.py"
13+
file.write_text("print('hello')", encoding="utf-8")
14+
15+
result = read_in_source_file(file)
16+
17+
assert result == "print('hello')"
18+
19+
20+
def test_reject_directory(tmp_path):
21+
with pytest.raises(SystemExit) as exc:
22+
read_in_source_file(tmp_path)
23+
24+
assert exc.value.code == 1
25+
26+
27+
def test_reject_non_py_file(tmp_path):
28+
file = tmp_path / "test.txt"
29+
file.write_text("not python", encoding="utf-8")
30+
31+
with pytest.raises(SystemExit) as exc:
32+
read_in_source_file(file)
33+
34+
assert exc.value.code == 1
35+
36+
37+
def test_file_read_error(monkeypatch, tmp_path):
38+
file = tmp_path / "test.py"
39+
file.write_text("content", encoding="utf-8")
40+
41+
def mock_open(*args, **kwargs):
42+
raise IOError("boom")
43+
44+
monkeypatch.setattr(Path, "open", mock_open)
45+
46+
with pytest.raises(SystemExit) as exc:
47+
read_in_source_file(file)
48+
49+
assert exc.value.code == 1
50+
51+
52+
def test_read_in_source_file_success(tmp_path):
53+
# Setup: Create a dummy .py file
54+
test_file = tmp_path / "script.py"
55+
content = "print('hello world')"
56+
test_file.write_text(content, encoding="utf-8")
57+
58+
# Action
59+
result = read_in_source_file(test_file)
60+
61+
# Assert
62+
assert result == content
63+
64+
def test_read_in_source_file_is_directory(tmp_path, capsys):
65+
# Setup: Use the tmp_path itself (which is a directory)
66+
with pytest.raises(SystemExit) as excinfo:
67+
read_in_source_file(tmp_path)
68+
69+
# Assert
70+
assert excinfo.value.code == 1
71+
captured = capsys.readouterr()
72+
assert "Error: The given path is a directory" in captured.out
73+
74+
def test_read_in_source_file_wrong_extension(tmp_path, capsys):
75+
# Setup: Create a text file instead of .py
76+
test_file = tmp_path / "notes.txt"
77+
test_file.write_text("not a python file")
78+
79+
with pytest.raises(SystemExit) as excinfo:
80+
read_in_source_file(test_file)
81+
82+
# Assert
83+
assert excinfo.value.code == 1
84+
captured = capsys.readouterr()
85+
assert "Error: The given file is not a Python (.py) file" in captured.out
86+
87+
def test_read_in_source_file_not_found(tmp_path, capsys):
88+
# Setup: A path that doesn't exist
89+
missing_file = tmp_path / "ghost.py"
90+
91+
with pytest.raises(SystemExit) as excinfo:
92+
read_in_source_file(missing_file)
93+
94+
# Assert
95+
assert excinfo.value.code == 1
96+
captured = capsys.readouterr()
97+
assert "Failed to read file" in captured.out

0 commit comments

Comments
 (0)