coretrace-python-analyzer 0.13.0__tar.gz → 0.15.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 (283) hide show
  1. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/CHANGELOG.md +20 -0
  2. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/PKG-INFO +1 -1
  3. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/docs/plugins.md +8 -0
  4. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/docs/usage.md +22 -5
  5. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/pyproject.toml +1 -1
  6. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/__init__.py +1 -1
  7. coretrace_python_analyzer-0.15.0/src/coretrace_python/abstract/strings.py +98 -0
  8. coretrace_python_analyzer-0.15.0/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +47 -0
  9. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/open_redirect/plugin.toml +1 -1
  10. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/ssrf/plugin.toml +1 -1
  11. coretrace_python_analyzer-0.15.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +49 -0
  12. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/advisories.py +6 -1
  13. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/correlation.py +36 -5
  14. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/graph.py +4 -2
  15. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/engine.py +9 -6
  16. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/detectors.py +13 -2
  17. coretrace_python_analyzer-0.15.0/src/coretrace_python/taint/urls.py +70 -0
  18. coretrace_python_analyzer-0.15.0/tests/test_advisory_url_host.py +145 -0
  19. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_detectors.py +4 -1
  20. coretrace_python_analyzer-0.15.0/tests/test_open_redirect_destinations.py +116 -0
  21. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_release.py +1 -1
  22. coretrace_python_analyzer-0.15.0/tests/test_ssrf_destinations.py +154 -0
  23. coretrace_python_analyzer-0.15.0/tests/test_url_proof.py +80 -0
  24. coretrace_python_analyzer-0.13.0/src/coretrace_python/bundled/security/open_redirect/open_redirect.py +0 -17
  25. coretrace_python_analyzer-0.13.0/src/coretrace_python/bundled/security/ssrf/ssrf.py +0 -17
  26. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/.github/workflows/ci.yml +0 -0
  27. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/.github/workflows/release.yml +0 -0
  28. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/.gitignore +0 -0
  29. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/LICENSE +0 -0
  30. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/NOTICE +0 -0
  31. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/README.md +0 -0
  32. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/docs/architecture.md +0 -0
  33. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/examples/add.py +0 -0
  34. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/examples/canonical_symbols.py +0 -0
  35. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/__main__.py +0 -0
  36. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/__init__.py +0 -0
  37. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/constants.py +0 -0
  38. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/heap.py +0 -0
  39. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/ranges.py +0 -0
  40. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/abstract/values.py +0 -0
  41. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/__init__.py +0 -0
  42. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/manager.py +0 -0
  43. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/analysis/provider.py +0 -0
  44. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +0 -0
  45. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/dependency_policy/plugin.toml +0 -0
  46. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +0 -0
  47. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +0 -0
  48. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/sample_advisories/plugin.toml +0 -0
  49. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +0 -0
  50. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +0 -0
  51. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +0 -0
  52. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/aiohttp/aiohttp_models.py +0 -0
  53. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/aiohttp/plugin.toml +0 -0
  54. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/bottle/bottle_models.py +0 -0
  55. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/bottle/plugin.toml +0 -0
  56. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/cli/cli_models.py +0 -0
  57. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/cli/plugin.toml +0 -0
  58. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/credentials/credential_models.py +0 -0
  59. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/credentials/plugin.toml +0 -0
  60. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/db_drivers/db_driver_models.py +0 -0
  61. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/db_drivers/plugin.toml +0 -0
  62. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/django/django_models.py +0 -0
  63. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/django/plugin.toml +0 -0
  64. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/fastapi/fastapi_models.py +0 -0
  65. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/fastapi/plugin.toml +0 -0
  66. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/flask/flask_models.py +0 -0
  67. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/flask/plugin.toml +0 -0
  68. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/http_clients/http_client_models.py +0 -0
  69. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/http_clients/plugin.toml +0 -0
  70. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/python_stdlib/plugin.toml +0 -0
  71. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py +0 -0
  72. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/sqlalchemy/plugin.toml +0 -0
  73. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +0 -0
  74. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/tornado/plugin.toml +0 -0
  75. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/models/tornado/tornado_models.py +0 -0
  76. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/config_secrets/config_secrets.py +0 -0
  77. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/config_secrets/plugin.toml +0 -0
  78. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +0 -0
  79. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +0 -0
  80. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/code_injection/code_injection.py +0 -0
  81. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/code_injection/plugin.toml +0 -0
  82. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/command_injection/command_injection.py +0 -0
  83. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/command_injection/plugin.toml +0 -0
  84. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +0 -0
  85. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/insecure_deserialization/plugin.toml +0 -0
  86. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/nosql_injection/nosql_injection.py +0 -0
  87. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/nosql_injection/plugin.toml +0 -0
  88. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/path_traversal/path_traversal.py +0 -0
  89. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/path_traversal/plugin.toml +0 -0
  90. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +0 -0
  91. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/plaintext_credentials/plugin.toml +0 -0
  92. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/sql_injection/plugin.toml +0 -0
  93. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/sql_injection/sql_injection.py +0 -0
  94. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/xss/plugin.toml +0 -0
  95. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/security/xss/xss.py +0 -0
  96. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +0 -0
  97. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +0 -0
  98. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/flask_debug/flask_debug.py +0 -0
  99. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/flask_debug/plugin.toml +0 -0
  100. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +0 -0
  101. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +0 -0
  102. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +0 -0
  103. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/insecure_tls/plugin.toml +0 -0
  104. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +0 -0
  105. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/missing_timeout/plugin.toml +0 -0
  106. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +0 -0
  107. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +0 -0
  108. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +0 -0
  109. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +0 -0
  110. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_crypto/plugin.toml +0 -0
  111. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +0 -0
  112. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_random/plugin.toml +0 -0
  113. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/bundled/syntax/weak_random/weak_random.py +0 -0
  114. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cache.py +0 -0
  115. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/__init__.py +0 -0
  116. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/builder.py +0 -0
  117. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/dominance.py +0 -0
  118. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cfg/model.py +0 -0
  119. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/cli.py +0 -0
  120. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/__init__.py +0 -0
  121. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/lattice.py +0 -0
  122. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dataflow/solver.py +0 -0
  123. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/__init__.py +0 -0
  124. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/policy.py +0 -0
  125. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/sbom.py +0 -0
  126. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/dependency/vex.py +0 -0
  127. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/__init__.py +0 -0
  128. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/baseline.py +0 -0
  129. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/coverage.py +0 -0
  130. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/model.py +0 -0
  131. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/provenance.py +0 -0
  132. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/refutation.py +0 -0
  133. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/findings/suppressions.py +0 -0
  134. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/__init__.py +0 -0
  135. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/ast_adapter.py +0 -0
  136. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/frontend/parser.py +0 -0
  137. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/__init__.py +0 -0
  138. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/nodes.py +0 -0
  139. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/hir/visitors.py +0 -0
  140. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/__init__.py +0 -0
  141. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/callgraph.py +0 -0
  142. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/modulegraph.py +0 -0
  143. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/interprocedural/summaries.py +0 -0
  144. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/__init__.py +0 -0
  145. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/defuse.py +0 -0
  146. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/lowering.py +0 -0
  147. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/model.py +0 -0
  148. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/printer.py +0 -0
  149. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/ir/ssa.py +0 -0
  150. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/__init__.py +0 -0
  151. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/api.py +0 -0
  152. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/loader.py +0 -0
  153. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/manifest.py +0 -0
  154. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/registry.py +0 -0
  155. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/plugins/secrets.py +0 -0
  156. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/__init__.py +0 -0
  157. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/json_format.py +0 -0
  158. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/report.py +0 -0
  159. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/sarif.py +0 -0
  160. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/reporters/text.py +0 -0
  161. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/__init__.py +0 -0
  162. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/identity.py +0 -0
  163. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/imports.py +0 -0
  164. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/scopes.py +0 -0
  165. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/semantic/symbols.py +0 -0
  166. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/__init__.py +0 -0
  167. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/manager.py +0 -0
  168. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/source/model.py +0 -0
  169. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/__init__.py +0 -0
  170. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/engine.py +0 -0
  171. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/models.py +0 -0
  172. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/routes.py +0 -0
  173. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/src/coretrace_python/taint/templates.py +0 -0
  174. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Chatbot.json +0 -0
  175. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/DjangoGoat.json +0 -0
  176. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Flask-Image-Editing-Website.json +0 -0
  177. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/InsecureGram.json +0 -0
  178. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/MiniBookApiServer.json +0 -0
  179. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/NPL-IA.json +0 -0
  180. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Spam-Detection-Project.json +0 -0
  181. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/Vulnerable_fastapi.json +0 -0
  182. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/dvpwa.json +0 -0
  183. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/full-stack-fastapi-template.json +0 -0
  184. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/healthchecks.json +0 -0
  185. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/httpie.json +0 -0
  186. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/luigi.json +0 -0
  187. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/my_gdb.json +0 -0
  188. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/network-topologer.json +0 -0
  189. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/pygoat.json +0 -0
  190. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/uploadbox-cloud-storage-solution.json +0 -0
  191. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/vulnerable-flask.json +0 -0
  192. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/expected/yt-fts.json +0 -0
  193. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/README.md +0 -0
  194. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/app.py +0 -0
  195. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/requirements.txt +0 -0
  196. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/fixtures/vulnerable-flask/vulnerable_services.py +0 -0
  197. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/regression/repositories.toml +0 -0
  198. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_advisory_evidence.py +0 -0
  199. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_advisory_feeds.py +0 -0
  200. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_aiohttp_models.py +0 -0
  201. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_analysis_manager.py +0 -0
  202. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_architecture.py +0 -0
  203. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_argument_conditions.py +0 -0
  204. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_attacker_arguments.py +0 -0
  205. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_audit_detectors.py +0 -0
  206. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_baseline.py +0 -0
  207. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_cache.py +0 -0
  208. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_cfg.py +0 -0
  209. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_class_keywords_with_targets.py +0 -0
  210. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_cli.py +0 -0
  211. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_closures.py +0 -0
  212. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_code_injection.py +0 -0
  213. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_config_secrets_coverage.py +0 -0
  214. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_constants.py +0 -0
  215. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_correlation.py +0 -0
  216. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_credential_context.py +0 -0
  217. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_dataflow.py +0 -0
  218. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_dependencies.py +0 -0
  219. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_discovery.py +0 -0
  220. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_http_clients.py +0 -0
  221. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_routes.py +0 -0
  222. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_django_safe_values.py +0 -0
  223. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_docs.py +0 -0
  224. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_dominance.py +0 -0
  225. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_emit_ir.py +0 -0
  226. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_exceptions.py +0 -0
  227. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_factories_match_loop_else.py +0 -0
  228. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_fail_on.py +0 -0
  229. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_fastapi_cli_precision.py +0 -0
  230. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_frameworks.py +0 -0
  231. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_frontend_gaps.py +0 -0
  232. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_heap.py +0 -0
  233. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_hex_digests.py +0 -0
  234. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_hir_builder.py +0 -0
  235. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_imports.py +0 -0
  236. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_interprocedural.py +0 -0
  237. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_interprocedural_taint.py +0 -0
  238. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_ir.py +0 -0
  239. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_match_patterns.py +0 -0
  240. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_members.py +0 -0
  241. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_module_level.py +0 -0
  242. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_nonlocal_writeback.py +0 -0
  243. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_nosql_injection.py +0 -0
  244. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_operator_sources.py +0 -0
  245. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_packaging.py +0 -0
  246. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_parallel.py +0 -0
  247. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_parser.py +0 -0
  248. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_plaintext_credentials.py +0 -0
  249. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_plugins.py +0 -0
  250. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_project.py +0 -0
  251. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_project_models.py +0 -0
  252. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_project_validators.py +0 -0
  253. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_provenance.py +0 -0
  254. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_pyir_imports.py +0 -0
  255. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_read_entry_points.py +0 -0
  256. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_real_code_precision.py +0 -0
  257. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_redirect_targets.py +0 -0
  258. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_refutation.py +0 -0
  259. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_refutation_evidence.py +0 -0
  260. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_regression.py +0 -0
  261. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_relative_paths.py +0 -0
  262. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_remaining_limits.py +0 -0
  263. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_reporters.py +0 -0
  264. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_safe_arguments.py +0 -0
  265. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_scale.py +0 -0
  266. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_scopes.py +0 -0
  267. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_secrets_sbom.py +0 -0
  268. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_self_state.py +0 -0
  269. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_source_manager.py +0 -0
  270. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_ssa.py +0 -0
  271. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_ssrf_targets.py +0 -0
  272. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_summary_sanitizers.py +0 -0
  273. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_suppressions.py +0 -0
  274. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_symbol_resolution.py +0 -0
  275. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_symbols.py +0 -0
  276. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntactic_rules.py +0 -0
  277. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_control_flow.py +0 -0
  278. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_coverage.py +0 -0
  279. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_syntax_expressions.py +0 -0
  280. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_taint.py +0 -0
  281. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_template_escaping.py +0 -0
  282. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_tornado_bottle_models.py +0 -0
  283. {coretrace_python_analyzer-0.13.0 → coretrace_python_analyzer-0.15.0}/tests/test_vex.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 (2026-09-26)
4
+
5
+ ### Precision
6
+
7
+ - `open-redirect` judges a flow on the destination a browser resolves from the target's constant text, with its own criteria rather than SSRF's. A reference on the current site keeps the browser there, so the flow is refuted. Such a reference is `/` followed by a character that is not a slash, a backslash, a space or a control character, as in `"/profile/" + id`; or `?`, `#`, or a relative path whose first segment holds no `:`. A fixed absolute host makes the flow a hotspot, since the input chooses the path, where that host may redirect again. Everything else stays a vulnerability, including `"/" + next`: `//evil.com` is another host. The URL facts both rules read are one shared proof, `coretrace_python.taint.urls`, and `ssrf` now reads it too, with the same verdicts. The regression corpus is unchanged: its three `open-redirect` findings redirect to values built without constant text (#168).
8
+
9
+ ### Advisories
10
+
11
+ - An advisory entry point may declare a `host` condition: the attacker must choose the host of the URL passed as the named argument. It is decided from the URL's constant text, the proof `ssrf` and `open-redirect` read. A fixed host with redirects disabled in the call leaves the call reachable, not exploitable. A fixed host with redirects followed keeps it exploitable, because a redirect may lead to a host the attacker controls, and the condition pending review says so. Input in the query does not choose the host. Anything else stays pending review, and so do the environment conditions. A `host` condition must name its argument and carry nothing else (#170).
12
+
13
+ ## 0.14.0 (2026-09-26)
14
+
15
+ ### Precision
16
+
17
+ - `ssrf` judges a flow on how `+` and f-strings build its URL. A fixed host is a limited proof: when constant text fixes `scheme://host` and ends the authority with `/`, `?` or `#` before any input, as in `"https://api.example.com/users/" + id`, the flow is a hotspot, because the input still chooses the path and a redirect may lead elsewhere. It is refuted only when the constant text fixes the path too, the input reaching the query or fragment only, and the call disables redirects (`allow_redirects=False`, `follow_redirects=False`). A module-level name counts as known text when the module binds it once to a string literal and never rebinds it. Everything else stays a vulnerability, including a base of unknown value such as an imported `BASE_URL`, which may leave the authority open. The advisory correlation keeps its own verdict. The regression corpus is unchanged: its six `ssrf` findings build their URL from values of unknown content (#165).
18
+
19
+ ### Plugins
20
+
21
+ - `TaintDetector.judge(ctx, function, flow, verdict)` lets a rule refine the refutation's verdict on a flow for its own findings, without changing the verdict other consumers of the flow read (#165).
22
+
3
23
  ## 0.13.0 (2026-09-26)
4
24
 
5
25
  ### Precision
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Summary: Static security analysis infrastructure for Python code
5
5
  Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
6
6
  Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
@@ -115,6 +115,14 @@ ordinary input reaching a database write is not a plaintext credential; `LOG`, f
115
115
  values that must not be written to a log as they are; and `PII`, for personal data
116
116
  that must not leave the application through a log, a third party or plain storage.
117
117
 
118
+ A rule may know more about its own sinks than the refutation does: `judge(ctx, function,
119
+ flow, verdict)` returns the rule's verdict on a flow, given the refutation's, and by
120
+ default returns it unchanged. `ssrf` and `open-redirect` read there what the URL's
121
+ constant text proves, a proof they share (`coretrace_python.taint.urls`) and each
122
+ judges by its own criteria. The refutation's
123
+ verdict, which other consumers of the flow such as the advisory correlation read, does
124
+ not change; a rule reading more analyses declares them in its `requires`.
125
+
118
126
  ### Security models: `ModelPlugin`
119
127
 
120
128
  Declares sources, sinks and sanitizers for a library or a framework. A model plugin
@@ -114,10 +114,10 @@ variable is not treated that way yet.
114
114
  | `sql-injection` | A database statement (`cursor.execute`, SQLAlchemy `text`, Django `raw`, …). Parameters of a parameterised query are not statements. |
115
115
  | `nosql-injection` | A NoSQL query, such as a MongoDB filter, given attacker input that may hold a structure: a JSON body giving `{"$ne": null}` matches any value. The input that may hold a structure is JSON, the raw bodies a `json.loads` decodes, files, WebSocket messages and standard input. Text cannot hold an operator: form fields, query parameters, headers, cookies, URL parameters, the command line, the environment, and an entry-point parameter annotated with scalars (`str`, `int`, `Optional[str]`, `Annotated[str, Query()]`, `list[str]`, …), which FastAPI validates. A Django, REST framework, aiohttp or tornado view reads a request object: its text attributes (`GET`, `POST`, `headers`, `query_params`, `query`, `match_info`, `arguments`, …) and the URL parameters after it are text, its body, files and `data` are not. Limits: a helper function the request is passed to reads it as a whole; the keys of a dict are not followed, so a string concatenated into `$where` is not reported. The analyzer ships no NoSQL sink: model plugins declare them. |
116
116
  | `path-traversal` | A file system path (`open`, `send_file`, `os.remove`, …). |
117
- | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. |
117
+ | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. How `+` and f-strings build the URL can prove part of its destination. When constant text fixes `scheme://host` and ends the authority with `/`, `?` or `#` before any input (`"https://api.example.com/users/" + id`, or a module-level string bound once and never rebound), the flow is a hotspot: the input still chooses the path, which may reach a sensitive resource, and a redirect may lead elsewhere. With the path fixed too, the input reaching the query or fragment only, and the call disabling redirects (`allow_redirects=False`, `follow_redirects=False`), the destination is fixed and the flow is refuted. Anything unproven stays a vulnerability: a base of unknown value (imported, a call result), a constant leaving the authority open (`"https:/"`, no `/` after the host, a variable port), `urljoin`, `%`, `format`, a URL built in a helper function. |
118
118
  | `xss` | An HTTP response body built without escaping. A Django template rendered by `render_to_string` escapes, where the analyzer can establish it (below). |
119
119
  | `insecure-deserialization` | `pickle`, `marshal`, `dill`, `jsonpickle`, `shelve`, and the YAML loaders (`yaml.load`, `load_all`, `full_load`, `unsafe_load` and their several-document versions); `yaml.load` and `load_all` given a safe loader (`SafeLoader`, `BaseLoader`) are not. |
120
- | `open-redirect` | An HTTP redirect target. |
120
+ | `open-redirect` | An HTTP redirect target, judged on the destination a browser resolves from the target's constant text, as `+` and f-strings build it. A reference on the current site keeps the browser there, where any further redirect is the project's own code, analysed on its own, so the flow is refuted. Such a reference is `/` followed by a character that is not a slash, a backslash, a space or a control character (`"/profile/" + id`), `?`, `#`, or a relative path whose first segment ends in the text without a `:`. A fixed absolute host (`"https://example.com/" + path`) sends the browser there, but the input chooses the path, where that host may redirect again: a hotspot. Anything else stays a vulnerability. That covers `"/" + next`, since `//evil.com` is another host, a host left open, a base of unknown value and a target built in a helper function. |
121
121
  | `plaintext-credential-storage` | A parameter named like a password stored in a database without hashing. Medium confidence, since a name is a hint. |
122
122
  | `exploitable-vulnerability` | An API affected by an advisory of a vulnerable requirement, see Dependencies. |
123
123
 
@@ -241,7 +241,7 @@ the directory of the checked file; a path outside it is printed as it is.
241
241
  ```json
242
242
  {
243
243
  "schema_version": 1,
244
- "tool": {"name": "coretrace-python-analyzer", "version": "0.13.0"},
244
+ "tool": {"name": "coretrace-python-analyzer", "version": "0.15.0"},
245
245
  "root": "/home/me/project",
246
246
  "findings": [
247
247
  {
@@ -273,7 +273,7 @@ bytes. A single-file check lists no components.
273
273
 
274
274
  ```json
275
275
  "tool": {
276
- "name": "coretrace-python-analyzer", "version": "0.13.0",
276
+ "name": "coretrace-python-analyzer", "version": "0.15.0",
277
277
  "components": [
278
278
  {"kind": "plugin", "name": "curated-advisories", "version": "2026.09.25", "digest": "sha256:…"},
279
279
  {"kind": "advisories", "name": "advisories.json", "digest": "sha256:…"}
@@ -386,6 +386,23 @@ a call passing a variable, or unpacking `*args` or `**kwargs`, leaves the condit
386
386
  pending review. A `semantic` condition — the document's tags, the platform, the
387
387
  template's origin — cannot be decided and is always pending review, never dropped.
388
388
 
389
+ A `host` condition says the attacker must choose the host of the URL passed as
390
+ `argument` (or at `position`), as credentials leaking to the host of a crafted URL
391
+ need: `{"kind": "host", "text": "…", "argument": "url", "position": 0}`. It is
392
+ decided from the URL's constant text, as `ssrf` and `open-redirect` read it, when the
393
+ attacker's input is passed in that argument. The outcome has three cases:
394
+
395
+ - Constant text fixes the host (`"https://api.example.com/" + path`) and the call
396
+ disables redirects (`allow_redirects=False`, `follow_redirects=False`): the call is
397
+ reachable, not exploitable.
398
+ - Constant text fixes the host but redirects may be followed: the call stays
399
+ exploitable. The fixed initial host does not clear the chain, since a redirect may
400
+ lead to a host the attacker controls, and the condition pending review says so.
401
+ Input in the query does not choose the host.
402
+ - Otherwise the condition stays pending review.
403
+
404
+ The other conditions, the environment included, stay pending as before.
405
+
389
406
  Every dependency finding records the highest level of evidence established in its
390
407
  `level` metadata: `declared` (the requirement allows a vulnerable version), `imported`
391
408
  (a module of the package is imported somewhere), `reachable` (an entry point or affected
@@ -436,7 +453,7 @@ project's behalf would go unseen. Without a lock file, a package is never
436
453
  `not_affected`. The document's `@id` derives from its statements, so two runs with the
437
454
  same result share it; its `author` is `Unknown Author`, as OpenVEX tools write when they
438
455
  cannot know it. Its `tooling` names the analyzer and each component with its digest
439
- (`coretrace-python-analyzer 0.13.0; curated-advisories 2026.09.25 (sha256:…)`), so the
456
+ (`coretrace-python-analyzer 0.15.0; curated-advisories 2026.09.25 (sha256:…)`), so the
440
457
  `@id` changes with the advisory data a statement was decided with.
441
458
 
442
459
  ## Large projects
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coretrace-python-analyzer"
7
- version = "0.13.0"
7
+ version = "0.15.0"
8
8
  description = "Static security analysis infrastructure for Python code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.13.0"
3
+ __version__ = "0.15.0"
4
4
 
@@ -0,0 +1,98 @@
1
+ """The constant text a string starts with (architecture §18): what ``+`` and f-strings
2
+ build from string literals and from module-level names bound once to one. Anything
3
+ else, a parameter, a call result, an import, ``%`` or ``format``, is text of unknown
4
+ value."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from types import MappingProxyType
10
+ from typing import ClassVar
11
+
12
+ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
13
+ from coretrace_python.cfg import CFGError
14
+ from coretrace_python.hir import nodes
15
+ from coretrace_python.ir.lowering import (
16
+ MODULE_BODY,
17
+ LoweringError,
18
+ PyIRAnalysis,
19
+ analyzable_functions,
20
+ )
21
+ from coretrace_python.ir.model import (
22
+ BinaryOp,
23
+ BuildString,
24
+ Constant,
25
+ ForNext,
26
+ Global,
27
+ Instruction,
28
+ SetGlobal,
29
+ StoreLocal,
30
+ Value,
31
+ )
32
+
33
+
34
+ def leading_text(value: Value, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> tuple[str, bool]:
35
+ """The constant text ``value`` starts with, and whether that text is all of it, given
36
+ the definitions of the function's values and the module's ``strings``."""
37
+
38
+ made = defs.get(value)
39
+ if isinstance(made, Constant):
40
+ return (made.value, True) if isinstance(made.value, str) else ("", False)
41
+ if isinstance(made, Global):
42
+ known = strings.get(made.name)
43
+ return ("", False) if known is None else (known, True)
44
+ if isinstance(made, BinaryOp) and made.operator == "add":
45
+ left, whole = leading_text(made.left, defs, strings)
46
+ if not whole:
47
+ return left, False
48
+ right, whole = leading_text(made.right, defs, strings)
49
+ return left + right, whole
50
+ if isinstance(made, BuildString):
51
+ text = ""
52
+ for part in made.parts:
53
+ piece, whole = leading_text(part, defs, strings)
54
+ text += piece
55
+ if not whole:
56
+ return text, False
57
+ return text, True
58
+ return "", False
59
+
60
+
61
+ class ModuleStringsAnalysis(Analysis[Mapping[str, str]]):
62
+ """The module-level names bound once, to a string literal, and never rebound: by no
63
+ other statement of the module body, no function assigning them as ``global``, no
64
+ definition of that name. A module that does not lower entirely has none."""
65
+
66
+ name: ClassVar[str] = "abstract.module_strings"
67
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
68
+
69
+ @classmethod
70
+ def compute(cls, ctx: AnalysisContext) -> Mapping[str, str]:
71
+ try:
72
+ functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
73
+ except (LoweringError, CFGError):
74
+ return MappingProxyType({})
75
+ body = next((function for function in functions if function.name == MODULE_BODY), None)
76
+ if body is None:
77
+ return MappingProxyType({})
78
+ defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
79
+ stored: dict[str, list[Value | None]] = {}
80
+ for block in body.blocks:
81
+ for instruction in block.instructions:
82
+ if isinstance(instruction, StoreLocal):
83
+ stored.setdefault(instruction.name, []).append(instruction.value)
84
+ if isinstance(block.terminator, ForNext):
85
+ # A ``for`` binds its target to each item.
86
+ stored.setdefault(block.terminator.target, []).append(None)
87
+ rebound = {
88
+ i.name for function in functions for block in function.blocks for i in block.instructions
89
+ if isinstance(i, SetGlobal)
90
+ }
91
+ rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
92
+ found: dict[str, str] = {}
93
+ for name, values in stored.items():
94
+ value = values[0] if len(values) == 1 and name not in rebound else None
95
+ made = defs.get(value) if value is not None else None
96
+ if isinstance(made, Constant) and isinstance(made.value, str):
97
+ found[name] = made.value
98
+ return MappingProxyType(found)
@@ -0,0 +1,47 @@
1
+ """Open redirect: attacker-controlled input reaching a open redirect sink.
2
+
3
+ The rule draws its verdict from what the target's constant text proves (``taint.urls``),
4
+ as a browser resolves it. A reference on the current site (``/profile/``, ``?page=``,
5
+ ``profile/``) keeps the browser on the site, where any further redirect is the project's
6
+ own code, analysed on its own: refuted. A fixed absolute host sends the browser there,
7
+ but the input chooses the path, where that host may redirect again: a hotspot. Anything
8
+ unproven, ``"/" + next`` included, stays a vulnerability.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import ClassVar
14
+
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
17
+ from coretrace_python.findings import Severity
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url
24
+
25
+
26
+ class OpenRedirectPlugin(TaintDetector):
27
+ name: ClassVar[str] = "open-redirect"
28
+ rule_id: ClassVar[str] = "open-redirect"
29
+ kind: ClassVar[TaintKind] = TaintKind.REDIRECT
30
+ severity: ClassVar[Severity] = Severity.MEDIUM
31
+ title: ClassVar[str] = "Open redirect"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ target = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if target.relative:
39
+ return Verdict(flow, Status.REFUTED, f"the browser stays on the site: the target starts with {target.text!r}")
40
+ if target.origin is not None and verdict.status is Status.VULNERABILITY:
41
+ return Verdict(
42
+ flow,
43
+ Status.HOTSPOT,
44
+ f"the browser goes to the host fixed by {target.origin!r}, but the input chooses the path, "
45
+ "where that host may redirect again",
46
+ )
47
+ return verdict
@@ -1,7 +1,7 @@
1
1
  name = "open-redirect"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.open-redirect"]
6
6
 
7
7
  [entrypoint]
@@ -1,7 +1,7 @@
1
1
  name = "ssrf"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.ssrf"]
6
6
 
7
7
  [entrypoint]
@@ -0,0 +1,49 @@
1
+ """Server-side request forgery: attacker-controlled input reaching a SSRF sink.
2
+
3
+ The rule draws its verdict from what the URL's constant text proves (``taint.urls``).
4
+ A fixed host keeps the input from choosing it, but the input still chooses the path,
5
+ which may reach a sensitive resource of that host, and a redirect may lead elsewhere: a
6
+ hotspot. With the path fixed too, the input reaching the query or fragment only, and
7
+ redirects disabled in the call, the destination is fixed: refuted. Anything unproven,
8
+ a base of unknown value or a URL built in a helper function, stays a vulnerability.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import ClassVar
14
+
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
17
+ from coretrace_python.findings import Severity
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url, redirects_disabled
24
+
25
+
26
+ class SsrfPlugin(TaintDetector):
27
+ name: ClassVar[str] = "ssrf"
28
+ rule_id: ClassVar[str] = "ssrf"
29
+ kind: ClassVar[TaintKind] = TaintKind.SSRF
30
+ severity: ClassVar[Severity] = Severity.HIGH
31
+ title: ClassVar[str] = "Server-side request forgery"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ url = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if url.origin is None:
39
+ return verdict
40
+ no_redirects = redirects_disabled(flow.sink_arguments)
41
+ if url.path_fixed:
42
+ if no_redirects:
43
+ return Verdict(flow, Status.REFUTED, f"the destination is fixed by {url.text!r} and redirects are disabled")
44
+ left = "a redirect may lead elsewhere"
45
+ else:
46
+ left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
47
+ if verdict.status is not Status.VULNERABILITY:
48
+ return verdict
49
+ return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {url.origin!r}, but {left}")
@@ -251,7 +251,7 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
251
251
  default = entry.get("default", False)
252
252
  if not isinstance(default, bool):
253
253
  raise TypeError(f"condition default must be true or false, got {default!r}")
254
- return Condition(
254
+ condition = Condition(
255
255
  str(entry["kind"]),
256
256
  str(entry["text"]),
257
257
  None if entry.get("argument") is None else str(entry["argument"]),
@@ -259,3 +259,8 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
259
259
  position,
260
260
  default,
261
261
  )
262
+ if condition.kind == "host" and (
263
+ (condition.argument is None and condition.position is None) or condition.values or condition.default
264
+ ):
265
+ raise ValueError("a host condition names the URL argument, by keyword or position, and nothing else")
266
+ return condition
@@ -10,7 +10,7 @@ are correlated here into one high-confidence ``exploitable-vulnerability`` findi
10
10
  from __future__ import annotations
11
11
 
12
12
  from collections.abc import Iterable, Mapping
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, replace
14
14
 
15
15
  from coretrace_python.dependency.graph import (
16
16
  DIRECT,
@@ -24,6 +24,7 @@ from coretrace_python.findings.refutation import Status, Verdict, Verdicts
24
24
  from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
25
25
  from coretrace_python.semantic.symbols import SymbolId
26
26
  from coretrace_python.taint import Sink, TaintFlow, TaintKind
27
+ from coretrace_python.taint.urls import UrlProof, redirects_disabled
27
28
 
28
29
  Affected = Mapping[SymbolId, tuple[Advisory, ...]]
29
30
 
@@ -61,14 +62,33 @@ class ConditionCheck:
61
62
  passed: str | None = None
62
63
 
63
64
 
64
- def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | None) -> ConditionCheck:
65
- """Decide ``entry``'s conditions against what the call's ``arguments`` denote."""
65
+ def check_conditions(
66
+ entry: AdvisoryEntryPoint | None,
67
+ arguments: Arguments | None,
68
+ url: UrlProof | None = None,
69
+ passed_as: frozenset[tuple[int | None, str | None]] = frozenset(),
70
+ ) -> ConditionCheck:
71
+ """Decide ``entry``'s conditions against what the call's ``arguments`` denote and, for
72
+ a ``host`` condition, what ``url`` proves of the value the attacker's input is passed
73
+ in (``passed_as``). A host the URL's constant text fixes rules the call out when the
74
+ call disables redirects; otherwise a redirect may still lead to a host the attacker
75
+ controls, and the condition stays pending with that uncertainty."""
66
76
 
67
77
  if entry is None:
68
78
  return ConditionCheck()
69
79
  met: list[Condition] = []
70
80
  pending: list[Condition] = []
71
81
  for condition in entry.conditions:
82
+ if condition.kind == "host":
83
+ origin = url.origin if url is not None and _passed_in(condition, passed_as) else None
84
+ if origin is None:
85
+ pending.append(condition)
86
+ elif redirects_disabled(arguments):
87
+ return ConditionCheck(tuple(met), tuple(pending), condition, repr(origin))
88
+ else:
89
+ text = f"the host is fixed by {origin!r}, but a redirect may lead to a host the attacker controls"
90
+ pending.append(replace(condition, text=text))
91
+ continue
72
92
  given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
73
93
  if given is None:
74
94
  pending.append(condition)
@@ -84,6 +104,16 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
84
104
  return ConditionCheck(tuple(met), tuple(pending))
85
105
 
86
106
 
107
+ def _passed_in(condition: Condition, passed_as: frozenset[tuple[int | None, str | None]]) -> bool:
108
+ """Whether a value passed as ``passed_as`` is the argument ``condition`` names."""
109
+
110
+ return any(
111
+ (keyword is not None and keyword == condition.argument)
112
+ or (keyword is None and position is not None and position == condition.position)
113
+ for position, keyword in passed_as
114
+ )
115
+
116
+
87
117
  def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
88
118
  """Which call a contradicted condition rules out, and why: ``app:12
89
119
  python.yaml.load(Loader=python.yaml.SafeLoader)``."""
@@ -120,9 +150,10 @@ def correlate(
120
150
  flows: Iterable[TaintFlow],
121
151
  verdicts: Verdicts | None,
122
152
  affected: Affected,
153
+ urls: Mapping[TaintFlow, UrlProof] | None = None,
123
154
  ) -> tuple[Finding, ...]:
124
155
  """Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
125
- one per advisory the sink is affected by."""
156
+ one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves."""
126
157
 
127
158
  findings: list[Finding] = []
128
159
  for flow in flows:
@@ -136,7 +167,7 @@ def correlate(
136
167
  entry = advisory.entry_point(flow.sink.symbol)
137
168
  if entry is not None and not any(entry.exploitable_through(p, k) for p, k in flow.passed_as):
138
169
  continue
139
- check = check_conditions(entry, flow.sink_arguments)
170
+ check = check_conditions(entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as)
140
171
  if check.contradicted is None:
141
172
  findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
142
173
  return tuple(findings)
@@ -151,8 +151,10 @@ class Condition:
151
151
  ``argument`` condition names the argument by keyword, and by ``position`` when it may
152
152
  be passed positionally, and lists the ``values`` that satisfy it — symbols
153
153
  (``python.yaml.FullLoader``) or constants as Python writes them (``True``);
154
- ``default`` says an absent argument means a vulnerable value. A ``semantic``
155
- condition cannot be checked and is reported as pending review."""
154
+ ``default`` says an absent argument means a vulnerable value. A ``host`` condition
155
+ says the attacker must choose the host of the URL passed as ``argument`` (or at
156
+ ``position``); the engine decides it from what the URL's constant text proves. A
157
+ ``semantic`` condition cannot be checked and is reported as pending review."""
156
158
 
157
159
  kind: str
158
160
  text: str
@@ -17,6 +17,7 @@ from typing import Any, ClassVar
17
17
 
18
18
  from coretrace_python import __version__
19
19
  from coretrace_python.abstract import ConstantPropagation, HeapAnalysis, RangeAnalysis
20
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
20
21
  from coretrace_python.analysis import (
21
22
  AnalysisContext,
22
23
  AnalysisManager,
@@ -125,6 +126,7 @@ from coretrace_python.taint import (
125
126
  escaped_templates,
126
127
  registered_routes,
127
128
  )
129
+ from coretrace_python.taint.urls import flow_url
128
130
 
129
131
  TOOL_NAME = "coretrace-python-analyzer"
130
132
 
@@ -143,6 +145,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
143
145
  DefUseAnalysis,
144
146
  ConstantPropagation,
145
147
  RangeAnalysis,
148
+ ModuleStringsAnalysis,
146
149
  HeapAnalysis,
147
150
  CallGraphAnalysis,
148
151
  SummaryAnalysis,
@@ -768,14 +771,14 @@ def _analyse_module(
768
771
  graph = manager.get(CallGraphAnalysis)
769
772
  correlated: list[Finding] = []
770
773
  if affected:
774
+ strings = manager.get(ModuleStringsAnalysis)
771
775
  for function in supported:
776
+ flows = manager.get(TaintAnalysis, function).flows
777
+ ssa = manager.get(SSAAnalysis, function)
778
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
779
+ urls = {flow: flow_url(flow, defs, strings) for flow in flows if flow.kinds & TaintKind.ADVISORY}
772
780
  correlated.extend(
773
- correlate(
774
- graph.name_of(function),
775
- manager.get(TaintAnalysis, function).flows,
776
- manager.get(RefutationAnalysis, function),
777
- affected,
778
- )
781
+ correlate(graph.name_of(function), flows, manager.get(RefutationAnalysis, function), affected, urls)
779
782
  )
780
783
  return CachedModule(
781
784
  manager.get(EntryPointAnalysis),
@@ -13,13 +13,14 @@ from typing import ClassVar
13
13
 
14
14
  from coretrace_python.analysis import AnyAnalysis
15
15
  from coretrace_python.findings import Confidence, Finding, Severity
16
- from coretrace_python.findings.refutation import RefutationAnalysis, Status
16
+ from coretrace_python.findings.refutation import RefutationAnalysis, Status, Verdict
17
+ from coretrace_python.hir import nodes
17
18
  from coretrace_python.interprocedural import CallGraphAnalysis
18
19
  from coretrace_python.ir.model import Call, Instruction, Symbol, Value
19
20
  from coretrace_python.ir.ssa import SSAAnalysis
20
21
  from coretrace_python.plugins.api import Plugin, PluginContext
21
22
  from coretrace_python.semantic.symbols import SymbolId
22
- from coretrace_python.taint import TaintAnalysis, TaintKind
23
+ from coretrace_python.taint import TaintAnalysis, TaintFlow, TaintKind
23
24
 
24
25
 
25
26
  class TaintDetector(Plugin):
@@ -40,6 +41,9 @@ class TaintDetector(Plugin):
40
41
  if not flow.kinds & self.kind:
41
42
  continue
42
43
  verdict = verdicts.verdict(flow)
44
+ if verdict.status is Status.REFUTED:
45
+ continue
46
+ verdict = self.judge(ctx, function, flow, verdict)
43
47
  if verdict.status is Status.REFUTED:
44
48
  continue
45
49
  message = f"{self.title}: {flow.source.label} input reaches {flow.sink.symbol}"
@@ -70,6 +74,13 @@ class TaintDetector(Plugin):
70
74
  )
71
75
  return findings
72
76
 
77
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
78
+ """This rule's verdict on ``flow``, given the refutation's: the same, unless the rule
79
+ knows more about its own sinks. The refutation's verdict, which other consumers
80
+ of the flow read, does not change."""
81
+
82
+ return verdict
83
+
73
84
 
74
85
  def _lower(confidence: Confidence) -> Confidence:
75
86
  return {Confidence.HIGH: Confidence.MEDIUM}.get(confidence, Confidence.LOW)
@@ -0,0 +1,70 @@
1
+ """What the constant text a URL starts with proves about its destination, and whether a
2
+ call follows redirects: one proof that the rules reading URLs share, each drawing its
3
+ own verdict from it. The text is what ``+`` and f-strings build before any other value
4
+ (``abstract.strings``); the proof says nothing about the rest of the URL."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+
12
+ from coretrace_python.abstract.strings import leading_text
13
+ from coretrace_python.interprocedural import Arguments
14
+ from coretrace_python.ir.model import Instruction, Value
15
+ from coretrace_python.taint.engine import TaintFlow
16
+
17
+ # ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
18
+ # URL parsers take for a slash, does not end it here.
19
+ _ORIGIN = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
20
+ # The same with the path fixed too: what follows is query or fragment.
21
+ _PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
22
+ # A reference a browser resolves on the current site: ``/`` then a character that is not
23
+ # a slash, a backslash, a space or a control character (``//`` and ``/\`` name another
24
+ # host, and a browser drops tabs and newlines), a query or a fragment, or a relative path
25
+ # whose first segment ends in the text without a ``:`` that would make it a scheme.
26
+ _RELATIVE = re.compile(r"/[^/\\\x00-\x20\x7f]|[?#]|[^/\\?#:\x00-\x20\x7f][^/\\?#:]*[/?#]")
27
+ # A client honours the one of these keywords it knows and rejects the other with a
28
+ # ``TypeError``: either way, the call follows no redirect.
29
+ _NO_REDIRECTS = ("allow_redirects", "follow_redirects")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class UrlProof:
34
+ """What the constant ``text`` a URL starts with proves: the ``origin`` it fixes
35
+ (``scheme://authority`` and the character ending it), whether it fixes the path
36
+ too (``path_fixed``), and whether it starts a reference a browser resolves on the
37
+ current site (``relative``)."""
38
+
39
+ text: str
40
+ origin: str | None = None
41
+ path_fixed: bool = False
42
+ relative: bool = False
43
+
44
+
45
+ def url_proof(text: str) -> UrlProof:
46
+ origin = _ORIGIN.match(text)
47
+ return UrlProof(
48
+ text,
49
+ origin.group(0) if origin is not None else None,
50
+ _PATH.match(text) is not None,
51
+ _RELATIVE.match(text) is not None,
52
+ )
53
+
54
+
55
+ def flow_url(flow: TaintFlow, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> UrlProof:
56
+ """The proof for the value ``flow`` passes to its sink, ``defs`` defining the values
57
+ of the function making the call. A flow reaching its sink in a callee proves nothing:
58
+ the value is built there."""
59
+
60
+ if flow.through is not None:
61
+ return UrlProof("")
62
+ text, _ = leading_text(flow.argument, defs, strings)
63
+ return url_proof(text)
64
+
65
+
66
+ def redirects_disabled(arguments: Arguments | None) -> bool:
67
+ """Whether the call disables redirects explicitly (``allow_redirects=False`` or
68
+ ``follow_redirects=False``); absent, a variable or unpacked options do not."""
69
+
70
+ return arguments is not None and any(arguments.given(name) == (True, "False") for name in _NO_REDIRECTS)