safeuploads 1.1.2__tar.gz → 2.0.0__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.
Files changed (131) hide show
  1. safeuploads-2.0.0/.agents/skills/conventional-commits/SKILL.md +86 -0
  2. safeuploads-2.0.0/.agents/skills/conventional-commits/references/pull-requests.md +19 -0
  3. safeuploads-2.0.0/.agents/skills/debugging/SKILL.md +23 -0
  4. safeuploads-2.0.0/.agents/skills/safeuploads-library/SKILL.md +53 -0
  5. safeuploads-2.0.0/.agents/skills/safeuploads-library/references/code-standards.md +45 -0
  6. safeuploads-2.0.0/.agents/skills/safeuploads-library/references/security.md +31 -0
  7. safeuploads-2.0.0/.agents/skills/safeuploads-library/references/testing.md +23 -0
  8. safeuploads-2.0.0/.agents/skills/skill-authoring/SKILL.md +53 -0
  9. safeuploads-2.0.0/.github/copilot-instructions.md +5 -0
  10. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/scripts/check_conventional_commits.py +2 -1
  11. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/audit-safeuploads.yml +1 -1
  12. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/conventional-commits.yml +3 -1
  13. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/docs.yml +1 -1
  14. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/fuzz.yml +1 -1
  15. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/lint-and-test.yml +30 -2
  16. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/publish-safeuploads.yml +2 -2
  17. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/test-matrix-safeuploads.yml +2 -2
  18. safeuploads-2.0.0/AGENTS.md +100 -0
  19. safeuploads-2.0.0/CHANGELOG.md +204 -0
  20. {safeuploads-1.1.2 → safeuploads-2.0.0}/CONTRIBUTING.md +21 -12
  21. {safeuploads-1.1.2 → safeuploads-2.0.0}/PKG-INFO +8 -6
  22. {safeuploads-1.1.2 → safeuploads-2.0.0}/README.md +7 -5
  23. safeuploads-2.0.0/SECURITY.md +17 -0
  24. {safeuploads-1.1.2 → safeuploads-2.0.0}/docs/index.md +7 -5
  25. {safeuploads-1.1.2 → safeuploads-2.0.0}/docs/rate-limiting.md +3 -2
  26. {safeuploads-1.1.2 → safeuploads-2.0.0}/docs/security/architecture.md +129 -62
  27. {safeuploads-1.1.2 → safeuploads-2.0.0}/docs/security/integration-checklist.md +54 -10
  28. safeuploads-2.0.0/docs/security/threat-model.md +595 -0
  29. {safeuploads-1.1.2 → safeuploads-2.0.0}/examples/README.md +15 -15
  30. {safeuploads-1.1.2 → safeuploads-2.0.0}/examples/fastapi_example.py +30 -20
  31. {safeuploads-1.1.2 → safeuploads-2.0.0}/pyproject.toml +10 -1
  32. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/__init__.py +22 -1
  33. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/audit.py +103 -10
  34. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/config.py +401 -110
  35. safeuploads-2.0.0/safeuploads/enums.py +677 -0
  36. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/exceptions.py +124 -19
  37. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/file_validator.py +247 -378
  38. safeuploads-2.0.0/safeuploads/inspectors/content_inspector.py +381 -0
  39. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/inspectors/gzip_inspector.py +53 -58
  40. safeuploads-2.0.0/safeuploads/inspectors/zip_inspector.py +1082 -0
  41. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/protocols.py +6 -5
  42. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/utils.py +121 -26
  43. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/validators/__init__.py +8 -0
  44. safeuploads-2.0.0/safeuploads/validators/compression_validator.py +1095 -0
  45. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/validators/extension_validator.py +57 -1
  46. safeuploads-2.0.0/safeuploads/validators/filename_validator.py +97 -0
  47. safeuploads-2.0.0/safeuploads/validators/fit_validator.py +221 -0
  48. safeuploads-2.0.0/safeuploads/validators/mime_validator.py +164 -0
  49. safeuploads-2.0.0/safeuploads/validators/signature_validator.py +110 -0
  50. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/validators/unicode_validator.py +59 -4
  51. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/validators/windows_validator.py +2 -2
  52. safeuploads-2.0.0/safeuploads/validators/xml_validator.py +143 -0
  53. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/conftest.py +65 -0
  54. safeuploads-2.0.0/tests/fuzz/helpers.py +77 -0
  55. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/fuzz/test_fuzz_filenames.py +19 -13
  56. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/fuzz/test_fuzz_images.py +13 -42
  57. safeuploads-2.0.0/tests/fuzz/test_fuzz_zip.py +161 -0
  58. safeuploads-2.0.0/tests/inspectors/test_content_inspector.py +508 -0
  59. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/inspectors/test_gzip_inspector.py +206 -11
  60. safeuploads-2.0.0/tests/inspectors/test_zip_inspector.py +2723 -0
  61. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_audit.py +354 -0
  62. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_config.py +228 -0
  63. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_config_validation.py +254 -10
  64. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_enums.py +57 -0
  65. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_exceptions.py +237 -7
  66. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_fastapi_integration.py +7 -3
  67. safeuploads-2.0.0/tests/test_file_validator.py +2338 -0
  68. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_performance.py +1 -1
  69. safeuploads-2.0.0/tests/test_utils.py +483 -0
  70. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_validate_activity_file.py +206 -24
  71. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/test_validate_gzip_file.py +28 -15
  72. safeuploads-2.0.0/tests/validators/test_compression_validator.py +2477 -0
  73. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/validators/test_extension_validator.py +156 -12
  74. safeuploads-2.0.0/tests/validators/test_filename_validator.py +73 -0
  75. safeuploads-2.0.0/tests/validators/test_fit_validator.py +210 -0
  76. safeuploads-2.0.0/tests/validators/test_mime_validator.py +147 -0
  77. safeuploads-2.0.0/tests/validators/test_signature_validator.py +132 -0
  78. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/validators/test_unicode_validator.py +86 -3
  79. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/validators/test_windows_validator.py +74 -18
  80. safeuploads-2.0.0/tests/validators/test_xml_validator.py +284 -0
  81. safeuploads-2.0.0/uv.lock +1339 -0
  82. safeuploads-1.1.2/.github/agents/se-gitops-ci-specialist.agent.md +0 -244
  83. safeuploads-1.1.2/.github/agents/se-security-reviewer.agent.md +0 -161
  84. safeuploads-1.1.2/.github/agents/test-implementor.agent.md +0 -168
  85. safeuploads-1.1.2/.github/agents/test-reviewer.agent.md +0 -113
  86. safeuploads-1.1.2/.github/copilot-instructions.md +0 -93
  87. safeuploads-1.1.2/.github/instructions/agents.instructions.md +0 -991
  88. safeuploads-1.1.2/.github/instructions/github-actions-ci-cd-best-practices.instructions.md +0 -607
  89. safeuploads-1.1.2/.github/instructions/instructions.instructions.md +0 -256
  90. safeuploads-1.1.2/.github/instructions/prompt.instructions.md +0 -88
  91. safeuploads-1.1.2/.github/instructions/python.instructions.md +0 -140
  92. safeuploads-1.1.2/.github/instructions/security-and-owasp.instructions.md +0 -51
  93. safeuploads-1.1.2/CHANGELOG.md +0 -114
  94. safeuploads-1.1.2/SECURITY.md +0 -17
  95. safeuploads-1.1.2/docs/security/threat-model.md +0 -334
  96. safeuploads-1.1.2/safeuploads/enums.py +0 -567
  97. safeuploads-1.1.2/safeuploads/inspectors/content_inspector.py +0 -214
  98. safeuploads-1.1.2/safeuploads/inspectors/zip_inspector.py +0 -645
  99. safeuploads-1.1.2/safeuploads/validators/compression_validator.py +0 -472
  100. safeuploads-1.1.2/safeuploads/validators/xml_validator.py +0 -102
  101. safeuploads-1.1.2/tests/fuzz/test_fuzz_zip.py +0 -116
  102. safeuploads-1.1.2/tests/inspectors/test_content_inspector.py +0 -204
  103. safeuploads-1.1.2/tests/inspectors/test_zip_inspector.py +0 -1227
  104. safeuploads-1.1.2/tests/test_file_validator.py +0 -1532
  105. safeuploads-1.1.2/tests/test_utils.py +0 -247
  106. safeuploads-1.1.2/tests/validators/test_compression_validator.py +0 -862
  107. safeuploads-1.1.2/tests/validators/test_xml_validator.py +0 -175
  108. safeuploads-1.1.2/uv.lock +0 -1308
  109. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/CODEOWNERS +0 -0
  110. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/FUNDING.yml +0 -0
  111. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  112. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  113. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  114. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  115. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/dependabot.yml +0 -0
  116. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/scripts/smoke_import.py +0 -0
  117. {safeuploads-1.1.2 → safeuploads-2.0.0}/.github/workflows/mirror.yml +0 -0
  118. {safeuploads-1.1.2 → safeuploads-2.0.0}/.gitignore +0 -0
  119. {safeuploads-1.1.2 → safeuploads-2.0.0}/CODE_OF_CONDUCT.md +0 -0
  120. {safeuploads-1.1.2 → safeuploads-2.0.0}/LICENSE.md +0 -0
  121. {safeuploads-1.1.2 → safeuploads-2.0.0}/docs/api.md +0 -0
  122. {safeuploads-1.1.2 → safeuploads-2.0.0}/mkdocs.yml +0 -0
  123. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/inspectors/__init__.py +0 -0
  124. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/inspectors/base.py +0 -0
  125. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/py.typed +0 -0
  126. {safeuploads-1.1.2 → safeuploads-2.0.0}/safeuploads/validators/base.py +0 -0
  127. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/__init__.py +0 -0
  128. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/fuzz/__init__.py +0 -0
  129. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/fuzz/test_fuzz_config.py +0 -0
  130. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/inspectors/__init__.py +0 -0
  131. {safeuploads-1.1.2 → safeuploads-2.0.0}/tests/validators/__init__.py +0 -0
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: conventional-commits
3
+ description: Write commit messages, commit titles and pull request titles/descriptions for safeuploads following Conventional Commits. Use whenever creating a git commit, amending or squashing commits, or opening/updating a pull request.
4
+ ---
5
+
6
+ # Conventional Commits for safeuploads
7
+
8
+ Every commit subject and every PR title is validated in CI by `.github/workflows/conventional-commits.yml`
9
+ (script: `.github/scripts/check_conventional_commits.py`). A non-compliant title blocks the PR, so get it
10
+ right the first time.
11
+
12
+ ## Header format
13
+
14
+ ```
15
+ <type>(<scope>)!: <description>
16
+ ```
17
+
18
+ - `(<scope>)` and `!` are optional.
19
+ - A single space after the colon is required.
20
+ - Only the first line is validated; the body and footers are free-form.
21
+
22
+ ### Allowed types (project whitelist)
23
+
24
+ | Type | Use for |
25
+ | --- | --- |
26
+ | `feat` | New public capability |
27
+ | `fix` | Bug fix (including security fixes) |
28
+ | `docs` | Documentation only (`docs/`, README, comments) |
29
+ | `refactor` | Code change that neither fixes a bug nor adds a feature |
30
+ | `perf` | Performance improvement |
31
+ | `test` | Adding or correcting tests |
32
+ | `build` | Build system, dependencies, packaging |
33
+ | `ci` | CI configuration and workflows |
34
+ | `chore` | Maintenance that fits nowhere else (version bumps, housekeeping) |
35
+ | `style` | Formatting only, no logic change |
36
+ | `revert` | Reverting a previous commit |
37
+
38
+ Any other type (e.g. `feature`, `bugfix`, `security`, `deps`) fails validation.
39
+
40
+ ### Scope
41
+
42
+ Optional, any non-empty string without parentheses.
43
+ Prefer the package or area touched, matching what recent history uses: `validators`, `inspectors`, `config`, `audit`, `utils`, `docs`, `deps`.
44
+ Omit the scope when the change spans many areas.
45
+
46
+ ### Description
47
+
48
+ - Imperative mood, describes what the change does: `add polyglot check`, not `added` / `adds`.
49
+ - Specific, not vague (`fix: bound nested archive depth`, not `fix: bug`).
50
+ - No trailing period; keep the header readable (aim for ~72 characters or fewer).
51
+
52
+ ### Breaking changes
53
+
54
+ Mark with `!` after the type/scope and/or a `BREAKING CHANGE: <explanation>` footer in the body.
55
+ Use whenever the public API (see `docs/api-stability.md` where it exists), a configuration field, an error code or the stored schema changes in a way that requires action from users.
56
+
57
+ ## Examples
58
+
59
+ ```
60
+ feat(validators): add a polyglot signature check
61
+ fix(inspectors): bound nested archive depth
62
+ docs: update the integration checklist
63
+ test(fuzz): add regression test for a Unicode filename
64
+ refactor(config)!: rename SecurityLimits.max_size to max_file_size
65
+ chore: bump version to 1.1.4
66
+ ```
67
+
68
+ ## Committing
69
+
70
+ 1. Inspect the change first: `git status`, `git diff --staged` (and `git log --format=%s -10` to match recent style and scopes).
71
+ 2. Choose the type by the *primary intent* of the change. If a diff mixes unrelated concerns, split it into separate commits, one concern each.
72
+ 3. Write the header per the rules above. Add a body (blank line after the header) only when the *why* isn't obvious; wrap at ~72 columns.
73
+ 4. Reference issues in the body/footer, e.g. `Closes #123` or `Refs #123`.
74
+ 5. Do not use `--no-verify`, and do not rewrite or force-push shared history unless explicitly asked.
75
+ 6. Include any attribution/co-author trailer that the session instructs you to add.
76
+
77
+ Optionally self-check before committing:
78
+
79
+ ```bash
80
+ python3 .github/scripts/check_conventional_commits.py "feat(scope): your header"
81
+ ```
82
+
83
+ ## Pull requests
84
+
85
+ Opening or updating a PR (title, template, `gh pr create`): read `references/pull-requests.md`.
86
+ A PR title must itself be a valid header; every non-merge commit subject on the branch is validated too.
@@ -0,0 +1,19 @@
1
+ # Pull requests
2
+
3
+ - **PR title**: must itself be a valid Conventional Commit header (same rules and whitelist). If the PR
4
+ has a single commit, reuse its header. If it has several, write one header that summarizes the whole
5
+ change, with the type of the most significant change.
6
+ - **Commits in the PR**: every non-merge commit subject on the branch is validated too, so make sure each
7
+ one complies before pushing (`git log --no-merges --format=%s origin/main..HEAD`). Fix offenders
8
+ with an interactive-free approach (e.g. `git commit --amend` for the last commit) or ask the user.
9
+ - **PR description**: fill in `.github/PULL_REQUEST_TEMPLATE.md`:
10
+ - *What does this PR do?* - clear description of the change and why.
11
+ - *Related issue* - `Closes #XYZ` so it auto-closes; required for anything beyond a small fix/typo.
12
+ - *Type of change* - tick the matching boxes.
13
+ - *Checklist* - tick only what is true (single concern, ~300 lines max excluding lockfiles/migrations/
14
+ generated code, tests and docs updated, Conventional Commits followed).
15
+ - *Notes for the reviewer* - tricky areas, alternatives considered, screenshots.
16
+ - A `feat` PR, a refactor or a public API change needs prior discussion in an issue, linked in the description (see `CONTRIBUTING.md`).
17
+ - A public API change also needs a `CHANGELOG.md` entry.
18
+ - Base branch is `main`. Create the PR with `gh pr create --title "<header>" --body "<filled template>"`.
19
+ Pushing and opening a PR are outward-facing: only do so when the user asked.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: debugging
3
+ description: Root-cause workflow for bugs, failing tests and CI failures. Use before changing code to fix unexpected behavior.
4
+ ---
5
+
6
+ # Debugging
7
+
8
+ ## Order of work
9
+
10
+ 1. Reproduce the failure with a command or a test; if you cannot reproduce it, say so instead of guessing.
11
+ 2. Read the full error and trace it to the first wrong value or call, not the line that raised.
12
+ 3. State the cause in one sentence and confirm it with evidence (a log, a failing assertion, a minimal call).
13
+ 4. For a behavior bug, write a failing test that captures it (`safeuploads-library` `references/testing.md`); for a lint, type or environment failure, the original failing command is the check.
14
+ 5. Fix the cause with the smallest change; do not patch the symptom or widen the change.
15
+ 6. Run the new test, then the area's tests, then the checks listed in the `safeuploads-library` skill.
16
+
17
+ ## Rules
18
+
19
+ - Change one thing at a time; revert an attempt that did not help before trying the next.
20
+ - After two failed fixes, stop and re-read the evidence; the diagnosis is probably wrong.
21
+ - Never skip, loosen or delete a test to get green, and never lower `fail_under` to pass the coverage gate.
22
+ - Reproduce a CI failure locally with the job's command when feasible; if runner-specific conditions block that, record the CI evidence and verify the fix in CI.
23
+ - Report what was verified and what was not; do not say "fixed" without a passing run.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: safeuploads-library
3
+ description: safeuploads library conventions (validators, inspectors, config, fail-closed security). Use when adding, moving, refactoring or reviewing code in safeuploads/ or tests/.
4
+ ---
5
+
6
+ # safeuploads library
7
+
8
+ `docs/security/architecture.md` states the pipeline and component roles, `docs/security/threat-model.md` the threats each check answers and `docs/api.md` the public API.
9
+ Where this skill and any of them disagree, they win.
10
+
11
+ ## Core rules (always apply)
12
+
13
+ - Fail closed: reject what cannot be positively accepted; never turn a failed check into a log line.
14
+ - One validator or inspector per concern, extending `BaseValidator` where it validates; `FileValidator` orchestrates and holds no check logic of its own.
15
+ - A new limit or allow-list entry lives in `FileSecurityConfig` or `SecurityLimits`, with its cross-field validation and a docs entry.
16
+ - Never loosen a default (limit, allow-list, block-list, ratio, depth) to make a test or one caller pass; ask first.
17
+ - Keep the filename check order: Unicode sanitization, then Windows reserved names, then the remaining normalization.
18
+ - Raise a `safeuploads.exceptions` subclass with its `ErrorCode`; the message is safe to show a caller and never contains file contents or filesystem paths.
19
+ - Blocking work (archive inspection, hashing, magic) stays off the event loop; use the existing worker-thread offload and `executor` option.
20
+ - A released `ErrorCode` never changes meaning; a new failure mode gets a new code.
21
+ - A new public name goes in `safeuploads/__init__.py` (`__all__`), `docs/api.md`, the README example if it changes usage, and `CHANGELOG.md`.
22
+ - A rename, removal, signature change, default change that accepts or rejects different input, or `ErrorCode` change is breaking: mark it `!` and say so in the changelog.
23
+ - KISS, YAGNI, DRY: search the package for an existing helper first; add no parameter, protocol or config field without a current caller; extract on the third repetition.
24
+ - Comments and docstrings say what the code does now and why; history goes in the commit message.
25
+
26
+ ## Read on demand
27
+
28
+ | When you are | Read |
29
+ | --- | --- |
30
+ | Writing code, docstrings, logging, errors or typing | `references/code-standards.md` |
31
+ | Writing or fixing tests, fuzzing or corpus work | `references/testing.md` |
32
+ | Adding or changing a check, limit, parser or anything on the threat model | `references/security.md` |
33
+
34
+ ## Before finishing
35
+
36
+ ```bash
37
+ uv run ruff check safeuploads tests examples
38
+ uv run ruff format --check safeuploads tests examples
39
+ uv run mypy
40
+ uv run pytest
41
+ ```
42
+
43
+ `uv run pytest` enforces the 90% branch-coverage gate.
44
+ `uv run ty check` is advisory in CI and not a gate.
45
+ `libmagic` must be installed on the machine (`brew install libmagic` or `apt-get install libmagic1`).
46
+
47
+ ## Repo hygiene
48
+
49
+ - Do only what was asked: no extra docs, summaries or READMEs.
50
+ - Prefer existing patterns and utilities over new files; keep it simple, build only what is needed now, do not repeat existing code.
51
+ - Do not add dependencies, and do not touch CI, publish configuration, supported Python versions or constraints unless asked.
52
+ - Local notes go in `devdocs/`, helper scripts in `devscripts/` (gitignored).
53
+ - Never run `git commit` unless asked; use the `conventional-commits` skill for messages.
@@ -0,0 +1,45 @@
1
+ # Code standards
2
+
3
+ ## Python
4
+ - Python 3.13 syntax (`requires-python = ">=3.13"`): `int | None`, `list[str]`, `dict[str, Any]`; no `typing.Optional/List/Dict`.
5
+ - Lines up to 79 characters, enforced by `ruff format` (`line-length = 79`); keep comments and docstrings within it too.
6
+ - Imports grouped stdlib / third-party / local; `TYPE_CHECKING` to avoid cycles.
7
+ - Every module has a top-level docstring.
8
+ - Comments and docstrings state what the code does now and why. History goes in the commit message.
9
+ - Performance and safety outrank brevity; correctness, explicit logging and clear validation messages matter.
10
+
11
+ ## Docstrings (PEP 257, Google sections)
12
+ Enforced by ruff `D` (`convention = "pep257"`) outside `tests/` and `examples/`.
13
+ - Summary on the opening line, then a blank line, then sections.
14
+ - Every public module, class, function and method has a docstring.
15
+ - `Args`: every parameter of a public function. A `_private` helper gets a summary and `Args` only for a non-obvious parameter.
16
+ - `Returns`: when a value comes back. `Returns: None.` is not required.
17
+ - `Raises`: the exceptions the function raises or deliberately lets propagate, especially the security ones. Omit it when there are none.
18
+ - Classes: summary plus `Attributes`.
19
+ - No examples in docstrings; they belong in `docs/` and `examples/`.
20
+ - A docstring is not a design document: what, then why, in the fewest lines.
21
+ - A docstring with backslashes is a raw string (`r"""`).
22
+
23
+ ## Typing
24
+ - `mypy` runs with `strict = true`; every def is fully and precisely typed, and a bare `Any` is a smell.
25
+ - Do not add `# type: ignore` or `# noqa` to pass; fix the type or the code. A `per-file-ignores` entry in `pyproject.toml` needs a comment saying why.
26
+ - Structural types for uploads live in `protocols.py` (`SeekableFile`, `UploadFileProtocol`); depend on them, not on FastAPI.
27
+
28
+ ## Async
29
+ - Keep validator entry points `async`; never call blocking I/O or CPU-heavy inspection directly in an `async def` (ruff `ASYNC` catches the common cases).
30
+ - Blocking inspection runs through `FileValidator`'s worker-thread offload.
31
+
32
+ ## Errors
33
+ - Raise the subclasses in `exceptions.py` with an `ErrorCode`; a configuration mistake raises `FileSecurityConfigurationError`.
34
+ - Keep the hierarchy: filename, size, MIME, signature, compression and processing errors each have their base; add a new subclass, not a new base.
35
+ - Do not swallow an exception from a check; an inspector that cannot finish rejects (`FileProcessingError` / `ResourceLimitError`).
36
+
37
+ ## Logging
38
+ - `logging.getLogger(__name__)` for diagnostics; security decisions go through `SecurityAuditLogger` with a correlation id.
39
+ - The host owns handlers, levels and formatting; never call `basicConfig`, add handlers or set levels.
40
+ - Never log file contents, or a filename that has not been sanitized.
41
+ - Log text and error wording are not public API.
42
+
43
+ ## Dependencies and extras
44
+ - Runtime dependencies stay `python-magic` and `defusedxml`; FastAPI is the `fastapi` extra and imported lazily.
45
+ - Adding or changing a dependency is a request-level decision: ask first.
@@ -0,0 +1,31 @@
1
+ # Security
2
+
3
+ `docs/security/threat-model.md` is the list of threats and `docs/security/integration-checklist.md` is what a host must do; update them when a change moves the boundary.
4
+
5
+ ## Fail closed
6
+ - Unknown, unparsable or unverifiable input is rejected.
7
+ - A check never degrades into a warning, a truncated inspection or a default "allow" because a limit was reached; hitting a limit is a rejection.
8
+ - Order matters: normalize and sanitize the name before any extension or reserved-name decision is made on it.
9
+
10
+ ## Filenames and paths
11
+ - Never build a filesystem path from an uploaded filename; the library returns a sanitized name and the host generates the storage name.
12
+ - Reject null bytes, control characters, bidirectional overrides, homoglyph tricks, double extensions, trailing dots/spaces and Windows reserved names.
13
+ - Compare extensions and MIME types case-insensitively against allow-lists; a block-list is a supplement, not the only gate.
14
+
15
+ ## Content
16
+ - Verify the MIME type and the file signature (`python-magic`) against the claimed extension; a mismatch is a rejection.
17
+ - Parse XML only with `defusedxml`; disable DTDs, external entities and network access (XXE, billion laughs).
18
+ - Inspect archives without extracting to disk, and bound compressed and uncompressed size, ratio, entry count, nesting depth and total work (ZIP bombs, recursive archives, gzip bombs).
19
+ - Treat embedded scripts, polyglot signatures and executable payloads in content analysis as hostile.
20
+
21
+ ## Resource limits
22
+ - Bound memory, CPU time and wall time per validation (`ResourceMonitor`, `SecurityLimits`); stream large files in chunks instead of reading them whole.
23
+ - A new limit has a conservative default, validation in `FileSecurityConfig`/`SecurityLimits` and a test on both sides of the boundary.
24
+ - Concurrency and rate limiting belong to the host (`docs/rate-limiting.md`); the library only bounds the cost of one validation.
25
+
26
+ ## Secrets and logging
27
+ - No secrets in code, tests or examples.
28
+ - Audit events carry a correlation id and sanitized identifiers; never file contents or raw, unsanitized filenames.
29
+ - Error messages are safe to show a client: no paths, stack detail or internal limits beyond what the code already exposes.
30
+
31
+ When you add a mitigation, state what it protects against; when you flag a vulnerability, explain the risk.
@@ -0,0 +1,23 @@
1
+ # Testing
2
+
3
+ - Tests live in `tests/`, files `test_*.py`, grouped in `Test*` classes; validator and inspector tests mirror the package under `tests/validators/` and `tests/inspectors/`; shared fixtures in `tests/conftest.py`.
4
+ - Every new check has at least one accepted payload and one hostile payload, and covers the boundary on both sides of each limit.
5
+ - Build hostile inputs in memory (ZIP bombs, nested archives, polyglots, XXE documents, Unicode tricks); never commit real malware or a large binary.
6
+ - Cover empty/None, zero-length, truncated, malformed and over-limit input, and assert the exception type and `ErrorCode`, not only that something raised.
7
+ - `with pytest.raises(...) as exc_info` for exceptions.
8
+ - Async tests: `async def test_*` (`asyncio_mode = "auto"`).
9
+ - Markers are declared in `pyproject.toml` (`unit`, `integration`, `slow`, `performance`, `fuzz`) and run with `--strict-markers`; mark a new test accordingly.
10
+ - Skips need `reason=`.
11
+ - Coverage is branch coverage with `fail_under = 90`; it is a ratchet, raise it when coverage rises and never lower it.
12
+ - Tests are deterministic and offline; a performance test asserts a generous bound, never an exact time.
13
+ - Do not mock a validator to test the orchestrator's decision; run the real one on a small payload.
14
+
15
+ ## Fuzzing
16
+ - Property tests use Hypothesis under `tests/fuzz/` (`test_fuzz_filenames.py`, `test_fuzz_zip.py`, `test_fuzz_images.py`, `test_fuzz_config.py`) and run weekly in CI (`fuzz.yml`) as well as in the suite.
17
+ - A fuzz failure that is a real bug becomes a fixed regression test with the minimal reproducer; do not only raise the example count or add `assume()` to hide it.
18
+
19
+ ```bash
20
+ uv run pytest tests/validators -v
21
+ uv run pytest -m "not performance and not fuzz"
22
+ uv run pytest --cov=safeuploads --cov-report=term-missing
23
+ ```
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: skill-authoring
3
+ description: How to create or edit skills in this repo. Use when adding, splitting, trimming or reviewing a file under .agents/skills.
4
+ ---
5
+
6
+ # Authoring skills for safeuploads
7
+
8
+ Goal: the least text loaded per task that still prevents the mistakes the skill exists for.
9
+
10
+ ## Where skills live
11
+
12
+ - Source of truth: `.agents/skills/<name>/SKILL.md`.
13
+ - Expose to Claude Code with a relative symlink: `ln -s ../../.agents/skills/<name> .claude/skills/<name>`.
14
+ - Name is kebab-case and matches the folder.
15
+
16
+ ## What is paid for, and when
17
+
18
+ | Part | Cost |
19
+ | --- | --- |
20
+ | Frontmatter `description` | Every session, whether used or not |
21
+ | `SKILL.md` body | Each time the skill is invoked |
22
+ | `references/*.md` | Only when the body says to read one and it does |
23
+
24
+ ## Rules
25
+
26
+ 1. **Description:** one sentence of what plus one sentence of when.
27
+ - No keyword lists; the trigger is the task, not synonyms.
28
+ - Keep it under about 200 characters.
29
+ 2. **Body:** only rules that apply to every use of the skill, ideally under 60 lines.
30
+ 3. **References:** put situational detail in `references/<topic>.md`, one topic per file.
31
+ - Link each from a "read when" table in `SKILL.md`; a reference nothing points to never loads.
32
+ - Do not nest references from references.
33
+ 4. **Do not duplicate a guide.** If `docs/` already states a rule, summarize it in one line and point to the doc; say which wins on conflict.
34
+ 5. **Rules, not history.** Leave out incident stories, rationale essays and changelog; keep the rule and, if it is non-obvious, a clause of why.
35
+ 6. **Imperative and checkable.** "Run `uv run ruff check .`" beats "ensure code quality".
36
+ 7. **Format:** one sentence per line (clean diffs), tables for lookups, code blocks for commands only.
37
+ 8. **One concern per skill.** Split by trigger, not by size; do not create several skills that always fire together.
38
+ 9. **No secrets or personal paths;** commands are repo-relative.
39
+ 10. **KISS, YAGNI, DRY:** no rule, reference or section for a hypothetical case; state each rule once and link to it.
40
+
41
+ ## Splitting an existing skill
42
+
43
+ 1. List what applies to every invocation; that stays in `SKILL.md`.
44
+ 2. Group the rest by the task that needs it; one reference file per group.
45
+ 3. Add the "read when" table and keep the always-apply rules above it.
46
+ 4. Check nothing was lost: every rule is in exactly one place.
47
+
48
+ ## Before finishing
49
+
50
+ - `wc -l` the body and each reference; reconsider anything over about 100 lines.
51
+ - Confirm the symlink resolves: `ls -L .claude/skills/<name>/SKILL.md`.
52
+ - Invoke the skill on a real task and confirm it reads the reference it should.
53
+ - Skills under `.agents/` and their `.claude/skills` symlinks are tracked; stage a new skill and its symlink together.
@@ -0,0 +1,5 @@
1
+ # safeuploads
2
+
3
+ The agent instructions for this repository live in [`AGENTS.md`](../AGENTS.md); follow it.
4
+
5
+ Detailed conventions are in the skills under [`.agents/skills/`](../.agents/skills/) and in `docs/`.
@@ -27,7 +27,8 @@ from collections.abc import Iterable
27
27
  # Allowed types. This whitelist is a *project policy* layered on top of the
28
28
  # Conventional Commits 1.0.0 spec (the spec recommends but does not mandate a
29
29
  # fixed set of types). Keep this list in sync with the project's commit
30
- # conventions documented in ``.github/copilot-instructions.md``.
30
+ # conventions documented in the ``conventional-commits`` skill
31
+ # (``.agents/skills``).
31
32
  #
32
33
  # Per the spec, types are matched case-insensitively.
33
34
  ALLOWED_TYPES: tuple[str, ...] = (
@@ -39,7 +39,7 @@ jobs:
39
39
  fetch-depth: 1
40
40
 
41
41
  - name: Install uv
42
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
42
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
43
43
  with:
44
44
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
45
45
  # and local development are held to the same floor and ceiling.
@@ -28,7 +28,9 @@ jobs:
28
28
  # before the commit-range check below can run.
29
29
  if: github.event.action != 'synchronize'
30
30
  # The PR title is untrusted data: passed through an environment variable
31
- # (never interpolated into the shell) to the trusted base-ref checker.
31
+ # (never interpolated into the shell) to the checker, which runs from the
32
+ # PR's own checkout. The job has read-only permissions
33
+ # (`contents: read`).
32
34
  env:
33
35
  PR_TITLE: ${{ github.event.pull_request.title }}
34
36
  run: |
@@ -40,7 +40,7 @@ jobs:
40
40
  fetch-depth: 1
41
41
 
42
42
  - name: Install uv
43
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
43
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
44
44
  with:
45
45
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
46
46
  # and local development are held to the same floor and ceiling.
@@ -28,7 +28,7 @@ jobs:
28
28
  run: sudo apt-get update && sudo apt-get install -y libmagic1
29
29
 
30
30
  - name: Install uv
31
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
31
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
32
32
  with:
33
33
  version-file: pyproject.toml
34
34
 
@@ -32,7 +32,7 @@ jobs:
32
32
  fetch-depth: 1
33
33
 
34
34
  - name: Install uv
35
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
35
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
36
36
  with:
37
37
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
38
38
  # and local development are held to the same floor and ceiling.
@@ -50,6 +50,34 @@ jobs:
50
50
  - name: Type check with mypy
51
51
  run: uv run mypy
52
52
 
53
+ # ----------------------------------------------------------------
54
+ # Type check with ty (advisory)
55
+ # ty is pre-1.0 and runs beside mypy for evaluation: findings are
56
+ # reported but never block a merge. mypy is the gate.
57
+ # ----------------------------------------------------------------
58
+ typecheck-ty:
59
+ name: Type check (ty, advisory)
60
+ runs-on: ubuntu-latest
61
+ continue-on-error: true
62
+
63
+ steps:
64
+ - name: Checkout code
65
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
66
+ with:
67
+ fetch-depth: 1
68
+
69
+ - name: Install uv
70
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
71
+ with:
72
+ version-file: pyproject.toml
73
+
74
+ - name: Install dependencies
75
+ # Same environment as the mypy step: ty resolves third-party types from it.
76
+ run: uv sync --frozen --dev --extra fastapi
77
+
78
+ - name: Type check (ty)
79
+ run: uv run --no-sync ty check --python .venv
80
+
53
81
  # ----------------------------------------------------------------
54
82
  # Test
55
83
  # Runs the full pytest suite with coverage. Only runs after lint
@@ -68,7 +96,7 @@ jobs:
68
96
  run: sudo apt-get update && sudo apt-get install -y libmagic1
69
97
 
70
98
  - name: Install uv
71
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
99
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
72
100
  with:
73
101
  version-file: pyproject.toml
74
102
 
@@ -36,7 +36,7 @@ jobs:
36
36
  persist-credentials: false
37
37
 
38
38
  - name: Install uv
39
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
39
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
40
40
  with:
41
41
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
42
42
  # and local development are held to the same floor and ceiling.
@@ -149,7 +149,7 @@ jobs:
149
149
  id-token: write
150
150
  steps:
151
151
  - name: Install uv
152
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
152
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
153
153
  with:
154
154
  # This job never checks the repository out, so there is no
155
155
  # pyproject.toml to read `required-version` from; keep this in step
@@ -51,7 +51,7 @@ jobs:
51
51
  run: sudo apt-get update && sudo apt-get install -y libmagic1
52
52
 
53
53
  - name: Install uv
54
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
54
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
55
55
  with:
56
56
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
57
57
  # and local development are held to the same floor and ceiling.
@@ -78,7 +78,7 @@ jobs:
78
78
  run: sudo apt-get update && sudo apt-get install -y libmagic1
79
79
 
80
80
  - name: Install uv
81
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
81
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
82
82
  with:
83
83
  # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
84
84
  # and local development are held to the same floor and ceiling.
@@ -0,0 +1,100 @@
1
+ # safeuploads
2
+
3
+ Secure file upload validation for Python 3.13+ applications.
4
+ Catches dangerous filenames, malicious extensions, Windows reserved names, compression bombs, polyglots and unsafe XML before an upload is accepted.
5
+ Runtime dependencies are minimal (`python-magic`, `defusedxml`; FastAPI behind the `fastapi` extra).
6
+ Published to PyPI; the public API is governed by SemVer.
7
+
8
+ ## Where the detail lives
9
+
10
+ | Working on | Read |
11
+ | --- | --- |
12
+ | Library code or tests | `.agents/skills/safeuploads-library/SKILL.md` |
13
+ | Fixing a bug, failing test or CI failure | `.agents/skills/debugging/SKILL.md` |
14
+ | Commit messages and PRs | `.agents/skills/conventional-commits/SKILL.md` |
15
+ | Writing or editing a skill | `.agents/skills/skill-authoring/SKILL.md` |
16
+ | Validation pipeline and component roles | `docs/security/architecture.md` |
17
+ | Threats and mitigations | `docs/security/threat-model.md` |
18
+ | What a host must do to integrate safely | `docs/security/integration-checklist.md` |
19
+ | Public API reference | `docs/api.md` |
20
+ | Rate limiting guidance | `docs/rate-limiting.md` |
21
+
22
+ Skills are plain Markdown; read the relevant `SKILL.md` and the reference files it points to when your tool does not load skills itself.
23
+
24
+ ## Layout
25
+
26
+ ```text
27
+ safeuploads/ Package: file_validator, config, exceptions, enums, audit, utils, validators/, inspectors/
28
+ tests/ Test suite (unit, integration, fuzz/, performance)
29
+ examples/ Runnable usage examples
30
+ docs/ Documentation site (mkdocs)
31
+ .agents/skills/ Agent skills (symlinked into .claude/skills)
32
+ ```
33
+
34
+ ## Commands
35
+
36
+ From the repository root (Python 3.13+, `uv`, system `libmagic`):
37
+
38
+ ```bash
39
+ uv sync --dev --extra fastapi
40
+ uv run ruff check safeuploads tests examples
41
+ uv run ruff format --check safeuploads tests examples
42
+ uv run mypy
43
+ uv run pytest
44
+ ```
45
+
46
+ ## Rules for every change
47
+
48
+ - Do only what was asked; no extra docs, summaries, READMEs or "helpful" files.
49
+ - Prefer existing patterns, utilities and files over new ones.
50
+ - KISS: pick the simplest design that solves the underlying problem; no indirection, abstraction layer or option without a current need. Simple means few moving parts, not a small diff or a patch over the symptom.
51
+ - YAGNI: build only what the task needs now; no speculative parameters, flags, hooks, config or extension points. What you do build must be complete and proper, not a stub or shortcut.
52
+ - DRY: reuse an existing helper, validator or fixture before writing another; extract shared code at the third repetition, not the second, and only when the copies change for the same reason.
53
+ - Do not add third-party dependencies, and do not change CI workflows, release or publish configuration, supported Python versions, dependency constraints or the `uv` settings unless asked.
54
+ - Never run `git commit` (or any commit-creating command) unless the user explicitly asks; staging for inspection is fine.
55
+ - Commit messages and PR titles follow Conventional Commits (enforced in CI); see the `conventional-commits` skill.
56
+ - Local-only notes go in `devdocs/`, helper scripts in `devscripts/` (both gitignored).
57
+ - No hardcoded secrets; real malware samples never enter the repository, tests build hostile payloads in memory.
58
+ - Ask for clarification when scope is unclear instead of assuming extra deliverables.
59
+ - A change touching the public API, the validation order or more than one validator/inspector starts with a short task list in chat for approval; keep plans in chat or `devdocs/`, never in tracked files.
60
+ - Bug fixes and new behavior start with a failing test, with both a valid and a malicious payload.
61
+ - A change to the public API is done only when `docs/api.md`, the affected docs page, `README.md` examples and `CHANGELOG.md` agree with the code.
62
+ - Done means the checks that apply to what changed ran and their output was read; say which ran and which did not (code checks do not apply to docs, `AGENTS.md` or skill-only changes).
63
+ - Before reporting a larger change done, compare the diff to the request and remove anything not asked for.
64
+
65
+ ## Fixing problems
66
+
67
+ - Fix the root cause, not the symptom. Before changing code, state the cause you found and the evidence for it.
68
+ - Do not add workarounds: no special-case branches, swallowed exceptions, retries or sleeps to hide a failure, `type: ignore` / `# noqa`, test skips or loosened assertions, or defensive checks that mask bad state upstream.
69
+ - If the proper fix is larger than the request (touches other validators, changes a boundary or the public API, needs a refactor), stop and ask before editing. Present the options:
70
+ 1. Proper fix: what it changes and why it is the right one.
71
+ 2. Minimal fix: what it leaves broken or the debt it creates.
72
+
73
+ Recommend one. Never apply the minimal option without the user's choice.
74
+ - If unsure whether a fix is a workaround, treat it as one and ask.
75
+ - A security-relevant weakening (a raised limit, a removed check, a widened allow-list, a skipped validation) is never a fix; ask first.
76
+
77
+ ## Building features
78
+
79
+ - Build the feature the way the architecture intends: correct component, layering and validation order, even when a shortcut would be quicker.
80
+ - Do not take shortcuts: no placeholder or stubbed logic, hardcoded values, TODOs standing in for required behavior, copy-pasted code instead of reuse, logic in the wrong layer, or skipped tests, validation, error handling or docs.
81
+ - Before writing code, state the approach and the files it touches. If more than one reasonable design exists, or the proper design is larger than the request implies, stop and ask. Present the options:
82
+ 1. Proper design: what it involves and why it fits the architecture.
83
+ 2. Simpler design: what it cuts or defers, and the debt it creates.
84
+
85
+ Recommend one. Never apply the simpler option without the user's choice.
86
+ - Scope still applies: complete means done properly, not bigger. Do not add features, options or files that were not requested.
87
+
88
+ ## Library invariants
89
+
90
+ - Fail closed: an input the library cannot positively accept is rejected, and a failing check never degrades to a warning.
91
+ - A validator is single-purpose and extends `BaseValidator`; `FileValidator` orchestrates, and a new check gets its own validator or inspector in the matching package.
92
+ - Configuration changes go through `FileSecurityConfig` and `SecurityLimits`, and a new field updates their cross-field validation.
93
+ - Failures raise the exceptions in `safeuploads/exceptions.py` (with a stable `ErrorCode`), never a bare `ValueError` or `Exception`.
94
+ - The order of filename checks is significant: Unicode sanitization first, Windows reserved names before other normalization.
95
+ - Blocking inspection never runs on the event loop; it goes through `FileValidator`'s worker-thread offload.
96
+ - XML is parsed only through `defusedxml` (XXE-safe); archives are inspected without extracting to disk and under size, ratio, depth and entry limits.
97
+ - Never build a filesystem path from an uploaded filename.
98
+ - Logging uses `logging.getLogger(__name__)` and the `SecurityAuditLogger`; never log file contents.
99
+ - Comments and docstrings state what the code does now, not what it used to do.
100
+ - Python 3.13 syntax, 79-character lines (`pyproject.toml`), PEP 257 docstrings with `Args`, `Returns`, `Raises` where applicable.