granttrace 2.6.0__tar.gz → 2.7.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 (181) hide show
  1. {granttrace-2.6.0 → granttrace-2.7.0}/CHANGELOG.md +32 -0
  2. {granttrace-2.6.0 → granttrace-2.7.0}/PKG-INFO +18 -4
  3. {granttrace-2.6.0 → granttrace-2.7.0}/README.md +17 -3
  4. {granttrace-2.6.0 → granttrace-2.7.0}/README.zh-CN.md +11 -2
  5. {granttrace-2.6.0 → granttrace-2.7.0}/SECURITY.md +8 -2
  6. {granttrace-2.6.0 → granttrace-2.7.0}/VERIFICATION.md +45 -0
  7. {granttrace-2.6.0 → granttrace-2.7.0}/api_sentinel.py +15 -10
  8. {granttrace-2.6.0 → granttrace-2.7.0}/config.schema.json +66 -0
  9. {granttrace-2.6.0 → granttrace-2.7.0}/core/__init__.py +1 -1
  10. {granttrace-2.6.0 → granttrace-2.7.0}/core/auditor.py +485 -36
  11. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/base.py +13 -0
  12. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/bola.py +138 -1
  13. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/identities.py +172 -0
  14. {granttrace-2.6.0 → granttrace-2.7.0}/core/evidence.py +2 -1
  15. {granttrace-2.6.0 → granttrace-2.7.0}/core/reporter.py +54 -2
  16. {granttrace-2.6.0 → granttrace-2.7.0}/core/request_builder.py +53 -6
  17. {granttrace-2.6.0 → granttrace-2.7.0}/core/results.py +3 -2
  18. {granttrace-2.6.0 → granttrace-2.7.0}/core/sarif.py +7 -1
  19. granttrace-2.7.0/core/scan_safety.py +214 -0
  20. {granttrace-2.6.0 → granttrace-2.7.0}/core/transport.py +2 -2
  21. {granttrace-2.6.0 → granttrace-2.7.0}/docs/comparison.md +4 -3
  22. {granttrace-2.6.0 → granttrace-2.7.0}/docs/configuration.md +100 -0
  23. {granttrace-2.6.0 → granttrace-2.7.0}/docs/windows-portable.md +15 -10
  24. {granttrace-2.6.0 → granttrace-2.7.0}/examples/README.md +4 -3
  25. {granttrace-2.6.0 → granttrace-2.7.0}/examples/sample_report.html +24 -24
  26. {granttrace-2.6.0 → granttrace-2.7.0}/examples/sample_result.json +19 -19
  27. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/PKG-INFO +18 -4
  28. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/SOURCES.txt +4 -0
  29. {granttrace-2.6.0 → granttrace-2.7.0}/pyproject.toml +1 -1
  30. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/build_windows_portable.py +12 -5
  31. granttrace-2.7.0/scripts/source_version.py +86 -0
  32. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_windows_portable.py +12 -3
  33. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_cli_security_hardening.py +1 -1
  34. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_output_snapshot.py +1 -0
  35. granttrace-2.7.0/tests/test_release_version.py +135 -0
  36. granttrace-2.7.0/tests/test_v27_features.py +1161 -0
  37. granttrace-2.7.0/tests/test_v27_hardening.py +680 -0
  38. granttrace-2.6.0/core/scan_safety.py +0 -127
  39. {granttrace-2.6.0 → granttrace-2.7.0}/CODE_OF_CONDUCT.md +0 -0
  40. {granttrace-2.6.0 → granttrace-2.7.0}/CONTRIBUTING.md +0 -0
  41. {granttrace-2.6.0 → granttrace-2.7.0}/LICENSE +0 -0
  42. {granttrace-2.6.0 → granttrace-2.7.0}/MANIFEST.in +0 -0
  43. {granttrace-2.6.0 → granttrace-2.7.0}/config.example.json +0 -0
  44. {granttrace-2.6.0 → granttrace-2.7.0}/core/authorization.py +0 -0
  45. {granttrace-2.6.0 → granttrace-2.7.0}/core/bopla.py +0 -0
  46. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_scaffold.py +0 -0
  47. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/__init__.py +0 -0
  48. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/readbacks.py +0 -0
  49. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validation/spec_checks.py +0 -0
  50. {granttrace-2.6.0 → granttrace-2.7.0}/core/config_validator.py +0 -0
  51. {granttrace-2.6.0 → granttrace-2.7.0}/core/demo.py +0 -0
  52. {granttrace-2.6.0 → granttrace-2.7.0}/core/demo_assets/__init__.py +0 -0
  53. {granttrace-2.6.0 → granttrace-2.7.0}/core/demo_assets/demo-config.json +0 -0
  54. {granttrace-2.6.0 → granttrace-2.7.0}/core/demo_assets/mock-data.json +0 -0
  55. {granttrace-2.6.0 → granttrace-2.7.0}/core/demo_assets/openapi.json +0 -0
  56. {granttrace-2.6.0 → granttrace-2.7.0}/core/diff.py +0 -0
  57. {granttrace-2.6.0 → granttrace-2.7.0}/core/generator.py +0 -0
  58. {granttrace-2.6.0 → granttrace-2.7.0}/core/models.py +0 -0
  59. {granttrace-2.6.0 → granttrace-2.7.0}/core/onboarding.py +0 -0
  60. {granttrace-2.6.0 → granttrace-2.7.0}/core/parameters.py +0 -0
  61. {granttrace-2.6.0 → granttrace-2.7.0}/core/parser.py +0 -0
  62. {granttrace-2.6.0 → granttrace-2.7.0}/core/remote_refs.py +0 -0
  63. {granttrace-2.6.0 → granttrace-2.7.0}/core/reproduction.py +0 -0
  64. {granttrace-2.6.0 → granttrace-2.7.0}/core/transactions.py +0 -0
  65. {granttrace-2.6.0 → granttrace-2.7.0}/core/yaml_safety.py +0 -0
  66. {granttrace-2.6.0 → granttrace-2.7.0}/docs/advanced.md +0 -0
  67. {granttrace-2.6.0 → granttrace-2.7.0}/docs/assets/report-finding.png +0 -0
  68. {granttrace-2.6.0 → granttrace-2.7.0}/docs/assets/report-preview.png +0 -0
  69. {granttrace-2.6.0 → granttrace-2.7.0}/docs/authorization-audit-hardening.md +0 -0
  70. {granttrace-2.6.0 → granttrace-2.7.0}/docs/authorization-coverage-audit.md +0 -0
  71. {granttrace-2.6.0 → granttrace-2.7.0}/docs/bopla-readonly.md +0 -0
  72. {granttrace-2.6.0 → granttrace-2.7.0}/docs/business-scenarios.md +0 -0
  73. {granttrace-2.6.0 → granttrace-2.7.0}/docs/case-studies/crapi.md +0 -0
  74. {granttrace-2.6.0 → granttrace-2.7.0}/docs/case-studies/vampi.md +0 -0
  75. {granttrace-2.6.0 → granttrace-2.7.0}/docs/history/release-checklist-history.md +0 -0
  76. {granttrace-2.6.0 → granttrace-2.7.0}/docs/history/release-verification-history.md +0 -0
  77. {granttrace-2.6.0 → granttrace-2.7.0}/docs/parameter-serialization.md +0 -0
  78. {granttrace-2.6.0 → granttrace-2.7.0}/docs/pr42-independent-review.md +0 -0
  79. {granttrace-2.6.0 → granttrace-2.7.0}/docs/pr42-state-authority-review.md +0 -0
  80. {granttrace-2.6.0 → granttrace-2.7.0}/docs/pr43-integration-plan.md +0 -0
  81. {granttrace-2.6.0 → granttrace-2.7.0}/docs/pr43-integration-review.md +0 -0
  82. {granttrace-2.6.0 → granttrace-2.7.0}/docs/release-checklist.md +0 -0
  83. {granttrace-2.6.0 → granttrace-2.7.0}/docs/remote-references.md +0 -0
  84. {granttrace-2.6.0 → granttrace-2.7.0}/examples/crapi/no-chatbot.compose.yml +0 -0
  85. {granttrace-2.6.0 → granttrace-2.7.0}/examples/crapi/prepare_config.py +0 -0
  86. {granttrace-2.6.0 → granttrace-2.7.0}/examples/vampi/prepare_config.py +0 -0
  87. {granttrace-2.6.0 → granttrace-2.7.0}/examples/vampi/run_vampi.py +0 -0
  88. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/dependency_links.txt +0 -0
  89. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/entry_points.txt +0 -0
  90. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/requires.txt +0 -0
  91. {granttrace-2.6.0 → granttrace-2.7.0}/granttrace.egg-info/top_level.txt +0 -0
  92. {granttrace-2.6.0 → granttrace-2.7.0}/mock_server/__init__.py +0 -0
  93. {granttrace-2.6.0 → granttrace-2.7.0}/mock_server/server.py +0 -0
  94. {granttrace-2.6.0 → granttrace-2.7.0}/openapi.json +0 -0
  95. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/benchmark_read_safety.py +0 -0
  96. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_authorization_coverage.py +0 -0
  97. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_business_scenarios.py +0 -0
  98. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_examples.py +0 -0
  99. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_install.py +0 -0
  100. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_pr42_adversarial.py +0 -0
  101. {granttrace-2.6.0 → granttrace-2.7.0}/scripts/verify_pr43_integration.py +0 -0
  102. {granttrace-2.6.0 → granttrace-2.7.0}/setup.cfg +0 -0
  103. {granttrace-2.6.0 → granttrace-2.7.0}/test_suite.py +0 -0
  104. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_auditor_integration.py +0 -0
  105. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_auth_cookie_references.py +0 -0
  106. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_auth_header_references.py +0 -0
  107. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_authorization_evidence_hardening.py +0 -0
  108. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_authorization_maintenance.py +0 -0
  109. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_authorization_policy_preflight.py +0 -0
  110. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bola_rejection_evidence.py +0 -0
  111. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_collection_grouping.py +0 -0
  112. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_integration.py +0 -0
  113. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_pending_exports.py +0 -0
  114. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_policy.py +0 -0
  115. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_policy_counterexamples.py +0 -0
  116. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_reports.py +0 -0
  117. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_bopla_transport_safety.py +0 -0
  118. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_business_authorization.py +0 -0
  119. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_cli_config_preflight.py +0 -0
  120. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_cli_report_boundaries.py +0 -0
  121. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_code_review_regressions.py +0 -0
  122. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_committed_examples.py +0 -0
  123. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_config_readback_preflight.py +0 -0
  124. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_config_scaffold.py +0 -0
  125. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_config_validation.py +0 -0
  126. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_credential_echo_redaction.py +0 -0
  127. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_curl_report.py +0 -0
  128. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_demo.py +0 -0
  129. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_diff.py +0 -0
  130. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_diff_hardening.py +0 -0
  131. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_english_reasons.py +0 -0
  132. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_evidence.py +0 -0
  133. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_expanded_detection.py +0 -0
  134. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_expanded_report_boundaries.py +0 -0
  135. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_first_run_guidance.py +0 -0
  136. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_header_cookie_array_object.py +0 -0
  137. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_header_cookie_boundaries.py +0 -0
  138. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_header_cookie_parameters.py +0 -0
  139. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_header_cookie_transport.py +0 -0
  140. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_late_private_value_redaction.py +0 -0
  141. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_mass_assignment_flow.py +0 -0
  142. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_merge_patch_recovery.py +0 -0
  143. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_module_boundaries.py +0 -0
  144. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_not_found_denial.py +0 -0
  145. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_onboarding.py +0 -0
  146. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_onboarding_cli.py +0 -0
  147. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_parameter_compatibility.py +0 -0
  148. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_parameter_serialization.py +0 -0
  149. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_parser_generator_hardening.py +0 -0
  150. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_parser_remote_refs.py +0 -0
  151. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_parser_replacement_auth.py +0 -0
  152. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_patch_readback_strictness.py +0 -0
  153. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_pr42_adversarial.py +0 -0
  154. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_pr42_state_authority.py +0 -0
  155. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_pr43_authorization_evidence.py +0 -0
  156. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_pr43_evidence_paths.py +0 -0
  157. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_pr43_put_safety.py +0 -0
  158. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_problem_details_denials.py +0 -0
  159. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_query_auth_parameters.py +0 -0
  160. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_rate_limit_retry.py +0 -0
  161. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_read_only_path_guards.py +0 -0
  162. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_read_only_safety.py +0 -0
  163. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_read_safety_cache.py +0 -0
  164. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_remote_ref_host_validation.py +0 -0
  165. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_remote_refs.py +0 -0
  166. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_remote_refs_hardening.py +0 -0
  167. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_remote_refs_premerge_audit.py +0 -0
  168. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_remote_refs_review_hardening.py +0 -0
  169. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_report_usability.py +0 -0
  170. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_reproduction.py +0 -0
  171. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_sarif.py +0 -0
  172. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_sarif_cli.py +0 -0
  173. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_scan_accounting.py +0 -0
  174. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_schema_regressions.py +0 -0
  175. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_small_review_items.py +0 -0
  176. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_spec_config_cross_checks.py +0 -0
  177. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_transaction_transport_hardening.py +0 -0
  178. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_transport.py +0 -0
  179. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_unconfigured_query_parameters.py +0 -0
  180. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_version_identity.py +0 -0
  181. {granttrace-2.6.0 → granttrace-2.7.0}/tests/test_yaml_alias_expansion.py +0 -0
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.0 - 2026-11-11
4
+
5
+ ### Added
6
+
7
+ - BOLA on query-style POST operations. A POST is sent only when its `bola`
8
+ policy declares `read_only: true` and gives the request body; its path,
9
+ operationId or summary must name a read action (search, query, lookup...)
10
+ and none of its metadata or concrete path may name a mutation. `body` is
11
+ sent unchanged by every identity; `identity_bodies` gives the Owner body
12
+ (repeated by the Visitor's cross request and Anonymous) and an optional
13
+ `visitor_self` body. Anything else is skipped or inconclusive with zero
14
+ requests, and a rate-limited POST is never retried.
15
+ - Sensitive-field leads (`detect_sensitive_exposure`, OWASP API3): unmasked
16
+ sensitive-named Owner values that reach the Visitor at the same JSON path.
17
+ `CONFIRMED` only with an explicit `deny` policy, a confirmed object-level
18
+ decision on the same observations and a value that differs from the
19
+ Visitor's own record; otherwise `SUSPICIOUS`. Values are never reported,
20
+ and the analysis is bounded on untrusted responses.
21
+ - Role matrix (`identities.roles`): for operations with a `required_role`,
22
+ one request per configured role, decided with the BFLA evidence standard
23
+ (valid privileged baseline, clean Anonymous denial, configured protected
24
+ data in the role's valid response). Shown in the HTML report and the JSON
25
+ `rbac_matrix`; confirmed violations are exported to SARIF as `GT-BFLA-001`,
26
+ confirmed sensitive-field findings as `GT-BOPLA-001`.
27
+
28
+ ### Changed
29
+
30
+ - The Windows portable build, its verifier, its workflow and the packaged
31
+ README and launcher read the version from `pyproject.toml` and
32
+ `core/__init__.py` (checked to agree) instead of repeating it. A test fails
33
+ on any drift.
34
+
3
35
  ## 2.6.0 - 2026-11-09
4
36
 
5
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: granttrace
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Evidence-driven, read-only authorization testing for OpenAPI APIs: BOLA/IDOR, broken authentication, BFLA and property-level checks
5
5
  Author: GrantTrace contributors
6
6
  License-Expression: Apache-2.0
@@ -64,6 +64,18 @@ is in the repository.
64
64
  - **Safe by default.** Scans are read-only. GET operations whose paths,
65
65
  identifiers or descriptions suggest side effects are skipped before any
66
66
  request, and writes against external targets are refused.
67
+ - **Read-only POST, only when you vouch for it.** A query-style POST (search,
68
+ lookup, query...) is compared across identities only after you declare
69
+ `read_only: true` and give the request body. Its path, operationId or summary
70
+ must name a read action and must not name a mutation; otherwise nothing is
71
+ sent. These checks reduce risk but cannot prove a POST has no side effects.
72
+ - **Sensitive-field leads.** With `detect_sensitive_exposure`, unmasked
73
+ sensitive-looking Owner fields (phone, salary, credentials...) that reach the
74
+ Visitor are reported as leads. They are confirmed only on top of a confirmed
75
+ object-level finding and never from a field name alone.
76
+ - **Role matrix.** Optional `identities.roles` are probed on operations with a
77
+ `required_role`. A lower role is a violation only when it receives the
78
+ privileged baseline's configured protected data, not merely an HTTP 200.
67
79
  - **Plan before you send.** `--init-config` drafts a configuration and a review
68
80
  checklist from the specification, `--validate-config` checks it offline, and
69
81
  `--dry-run` shows what would be requested while sending nothing.
@@ -82,8 +94,10 @@ is in the repository.
82
94
  | BOLA / IDOR | CWE-639 | Read-only, as Owner, Visitor and Anonymous | Two test accounts and a resource each of them owns |
83
95
  | Broken Authentication | CWE-306 | Read-only, on operations the specification protects | `private_data_paths`: Owner values Anonymous must not see |
84
96
  | Broken Function Level Authorization | CWE-285 | Read-only | The required role and each identity's role |
97
+ | Role matrix (RBAC) | CWE-285 | Read-only, one request per configured role | `identities.roles`, `required_role` and `private_data_paths` |
98
+ | BOLA on query-style POST | CWE-639 | Declared `read_only: true` POST with a read action in its name | The request `body` (or `identity_bodies`) |
85
99
  | Private data in collections | CWE-639 | Read-only, on list endpoints | `private_data_paths` |
86
- | Property-level authorization (BOPLA) | CWE-200 | Read-only | Per-field rules in `bopla` |
100
+ | Property-level authorization (BOPLA) | CWE-200 | Read-only | Per-field rules in `bopla`; `detect_sensitive_exposure` adds heuristic leads |
87
101
  | Mass Assignment | CWE-915 | **Built-in demo only** | – |
88
102
 
89
103
  Mass assignment testing writes to a resource, so GrantTrace only does it when it
@@ -93,8 +107,8 @@ before any write; the built-in demo, which owns its data, shows the complete
93
107
  flow with independent readback and verified rollback.
94
108
 
95
109
  **Not supported yet:** GraphQL and gRPC; OAuth, OIDC or mTLS login flows
96
- (configure tokens or headers yourself); API keys in the query string; POST and
97
- DELETE tests; writes against external APIs.
110
+ (configure tokens or headers yourself); API keys in the query string; POST
111
+ other than declared read-only queries; DELETE tests; writes against external APIs.
98
112
 
99
113
  ## Install
100
114
 
@@ -37,6 +37,18 @@ is in the repository.
37
37
  - **Safe by default.** Scans are read-only. GET operations whose paths,
38
38
  identifiers or descriptions suggest side effects are skipped before any
39
39
  request, and writes against external targets are refused.
40
+ - **Read-only POST, only when you vouch for it.** A query-style POST (search,
41
+ lookup, query...) is compared across identities only after you declare
42
+ `read_only: true` and give the request body. Its path, operationId or summary
43
+ must name a read action and must not name a mutation; otherwise nothing is
44
+ sent. These checks reduce risk but cannot prove a POST has no side effects.
45
+ - **Sensitive-field leads.** With `detect_sensitive_exposure`, unmasked
46
+ sensitive-looking Owner fields (phone, salary, credentials...) that reach the
47
+ Visitor are reported as leads. They are confirmed only on top of a confirmed
48
+ object-level finding and never from a field name alone.
49
+ - **Role matrix.** Optional `identities.roles` are probed on operations with a
50
+ `required_role`. A lower role is a violation only when it receives the
51
+ privileged baseline's configured protected data, not merely an HTTP 200.
40
52
  - **Plan before you send.** `--init-config` drafts a configuration and a review
41
53
  checklist from the specification, `--validate-config` checks it offline, and
42
54
  `--dry-run` shows what would be requested while sending nothing.
@@ -55,8 +67,10 @@ is in the repository.
55
67
  | BOLA / IDOR | CWE-639 | Read-only, as Owner, Visitor and Anonymous | Two test accounts and a resource each of them owns |
56
68
  | Broken Authentication | CWE-306 | Read-only, on operations the specification protects | `private_data_paths`: Owner values Anonymous must not see |
57
69
  | Broken Function Level Authorization | CWE-285 | Read-only | The required role and each identity's role |
70
+ | Role matrix (RBAC) | CWE-285 | Read-only, one request per configured role | `identities.roles`, `required_role` and `private_data_paths` |
71
+ | BOLA on query-style POST | CWE-639 | Declared `read_only: true` POST with a read action in its name | The request `body` (or `identity_bodies`) |
58
72
  | Private data in collections | CWE-639 | Read-only, on list endpoints | `private_data_paths` |
59
- | Property-level authorization (BOPLA) | CWE-200 | Read-only | Per-field rules in `bopla` |
73
+ | Property-level authorization (BOPLA) | CWE-200 | Read-only | Per-field rules in `bopla`; `detect_sensitive_exposure` adds heuristic leads |
60
74
  | Mass Assignment | CWE-915 | **Built-in demo only** | – |
61
75
 
62
76
  Mass assignment testing writes to a resource, so GrantTrace only does it when it
@@ -66,8 +80,8 @@ before any write; the built-in demo, which owns its data, shows the complete
66
80
  flow with independent readback and verified rollback.
67
81
 
68
82
  **Not supported yet:** GraphQL and gRPC; OAuth, OIDC or mTLS login flows
69
- (configure tokens or headers yourself); API keys in the query string; POST and
70
- DELETE tests; writes against external APIs.
83
+ (configure tokens or headers yourself); API keys in the query string; POST
84
+ other than declared read-only queries; DELETE tests; writes against external APIs.
71
85
 
72
86
  ## Install
73
87
 
@@ -30,6 +30,13 @@ Demo 在 `127.0.0.1` 启动一个临时 API,找出其中的 BOLA 和 Mass Assi
30
30
  证据不足时判为 `SUSPICIOUS` 或 `INCONCLUSIVE`,不会算作漏洞。
31
31
  - **默认安全。** 扫描只读。路径、operationId 或描述带有副作用语义的 GET 在请求前跳过,
32
32
  对外部目标的写入一律拒绝。
33
+ - **只读 POST 需由你声明。** 查询类 POST(search、lookup、query 等)只有在你声明 `read_only: true`
34
+ 并提供请求体后才会跨身份比较;其路径、operationId 或 summary 必须表明读取动作且不得表明修改,
35
+ 否则不发送任何请求。这些检查只能降低风险,不能证明 POST 没有副作用。
36
+ - **敏感字段线索。** 开启 `detect_sensitive_exposure` 后,Visitor 收到的未脱敏、名称敏感的 Owner 字段
37
+ (电话、薪资、凭据等)作为线索报告;只有在对象级越权已确认时才会确认,绝不仅凭字段名确认。
38
+ - **角色矩阵。** 可选的 `identities.roles` 只在配置了 `required_role` 的操作上探测。低权限角色只有拿到
39
+ 特权基线中配置的受保护数据才算违规,单纯的 HTTP 200 不算。
33
40
  - **先计划,再发送。** `--init-config` 根据规范生成配置草稿和核对清单,`--validate-config`
34
41
  离线校验,`--dry-run` 列出将要发送的请求,但不发送任何请求。
35
42
  - **报告可直接用于修复。** 每条结果说明比较了什么;每个确认漏洞附修复方向、复验步骤、
@@ -42,10 +49,12 @@ Demo 在 `127.0.0.1` 启动一个临时 API,找出其中的 BOLA 和 Mass Assi
42
49
  | 检查 | CWE | 运行方式 | 需要配置 |
43
50
  |---|---|---|---|
44
51
  | BOLA / IDOR | CWE-639 | 只读,以 Owner、Visitor、Anonymous 请求 | 两个测试账号及各自拥有的资源 |
52
+ | 查询类 POST 的 BOLA | CWE-639 | 声明 `read_only: true` 且名称表明读取动作的 POST | 请求体 `body`(或 `identity_bodies`) |
45
53
  | Broken Authentication | CWE-306 | 只读,针对规范要求认证的操作 | `private_data_paths`:匿名不应看到的 Owner 数据 |
46
54
  | Broken Function Level Authorization | CWE-285 | 只读 | 所需角色及各身份的角色 |
55
+ | 角色矩阵(RBAC) | CWE-285 | 只读,每个配置角色一次请求 | `identities.roles`、`required_role` 与 `private_data_paths` |
47
56
  | 集合接口私有数据泄露 | CWE-639 | 只读,针对列表接口 | `private_data_paths` |
48
- | 字段级授权(BOPLA) | CWE-200 | 只读 | `bopla` 中按字段的规则 |
57
+ | 字段级授权(BOPLA) | CWE-200 | 只读 | `bopla` 中按字段的规则;`detect_sensitive_exposure` 提供启发式线索 |
49
58
  | Mass Assignment | CWE-915 | **仅内置 demo** | – |
50
59
 
51
60
  Mass Assignment 测试需要写入资源,GrantTrace 只在能核验资源实际状态并恢复时才执行。
@@ -53,7 +62,7 @@ Mass Assignment 测试需要写入资源,GrantTrace 只在能核验资源实
53
62
  自己持有数据的内置 demo 展示完整流程,包括独立读回和已核验的恢复。
54
63
 
55
64
  **暂不支持:** GraphQL 和 gRPC;OAuth、OIDC、mTLS 登录流程(请自行配置 token 或 header);
56
- 查询字符串中的 API key;POST、DELETE 测试;对外部 API 的写入。
65
+ 查询字符串中的 API key;声明为只读的查询以外的 POST;DELETE 测试;对外部 API 的写入。
57
66
 
58
67
  ## 安装
59
68
 
@@ -9,8 +9,14 @@ cannot independently verify an external API's authoritative resource state, so
9
9
  even with `--allow-write-tests`, an explicit write allowlist, readback mappings
10
10
  and `consistency: "strong"`, allowlisted PATCH and PUT checks are reported as
11
11
  `INCONCLUSIVE` (`unverified_state_authority`) and send nothing. The built-in demo,
12
- which owns its data, is the only place the full flow runs. POST and DELETE are
13
- never sent as tests.
12
+ which owns its data, is the only place the full flow runs. DELETE is never sent.
13
+ A POST is sent only for the read-only BOLA comparison, and only when the
14
+ operation's policy declares `read_only: true` with an explicit request body, its
15
+ path, operationId or summary names a read action, and neither its metadata nor
16
+ its concrete path names a mutation. These checks refuse obviously
17
+ state-changing operations but cannot prove that a POST has no side effects:
18
+ declare `read_only` only for operations you have confirmed change nothing. A
19
+ POST is never retried.
14
20
 
15
21
  Where the flow does run, every operation must be in the explicit write
16
22
  allowlist, and active checks require a JSON request schema plus a configured GET
@@ -3,6 +3,51 @@
3
3
  Detailed historical release verification records are archived in
4
4
  [release verification history](docs/history/release-verification-history.md).
5
5
 
6
+ ## v2.7.0 pre-release acceptance - 2026-11-11
7
+
8
+ Status: **accepted for release; v2.7.0 is not published.** Release candidate:
9
+ commit `750a801` (head of PR #90: main `9fc560c`, the merge of #84, plus the
10
+ release date, the publishing workflow's manual choice and release-specific
11
+ documentation); the commit after it adds only this record. PyPI's latest is
12
+ 2.6.0; there is no `v2.7.0` tag or Release.
13
+
14
+ Environment: Linux x86_64 container; CPython **3.13.18** for coverage, linting,
15
+ acceptance scripts and the build, plus CPython **3.9.25** and **3.14.6**; PyYAML
16
+ **6.0.3**, pytest **9.2.0** (**8.4.2** on 3.9), coverage **7.17.2**, Ruff
17
+ **0.17.4**, Mypy **2.4.1**, build **1.6.1**, Twine **7.0.0**, setuptools
18
+ **84.2.1**, wheel **0.48.0**. The Windows package was built and checked by the
19
+ GitHub-hosted `windows-2025` runner; no Windows host was used locally.
20
+
21
+ | Check | Result |
22
+ |---|---|
23
+ | CI on main after #84 (`9fc560c`) | GrantTrace CI [run 42918318210](https://github.com/ysc070528/granttrace/actions/runs/42918318210) (Ruff, Mypy, tests on 3.9 / 3.12 / 3.14, runtime dependency audit), Windows x64 portable [run 42918318200](https://github.com/ysc070528/granttrace/actions/runs/42918318200) and CodeQL [run 42918318197](https://github.com/ysc070528/granttrace/actions/runs/42918318197): all passed |
24
+ | Full regression suite | 1322 passed and 2402 subtests passed on CPython 3.13.18 and 3.14.6; 1322 passed on 3.9.25; full unittest passed |
25
+ | Branch-aware coverage | 89.95% combined (statements 7281 / 7922, branches 3474 / 4034 = 86.12%); required minimum 80% |
26
+ | Acceptance scripts | PR42 adversarial; PR43 integration (89 tests, source `750a801`, clean tree); authorization coverage, authoritative and generic HTTP; business scenarios 6 / 6; JSON / YAML examples; installation |
27
+ | Wheel / sdist | Isolated `python -m build` from `git archive`; `twine check --strict` passed for both. Wheel: 43 files; sdist: 195 files; no caches, local configurations or build output. Metadata: `Version: 2.7.0`, the unchanged `Summary`, `Requires-Python: >=3.9`, `Requires-Dist: PyYAML>=6.0` |
28
+ | Clean installations | The wheel in new virtual environments on CPython 3.9.25, 3.13.18 and 3.14.6, and the sdist on 3.13.18, each outside the checkout: `granttrace --version` prints `GrantTrace 2.7.0` and `core` loads from `site-packages`; `granttrace demo` exits 0 with BOLA and mass assignment confirmed, rollback verified, the database restored and the server stopped. `scripts/verify_install.py --wheel` passed on the built wheel: versions consistent, 0 PATCH in read-only and external active scans (the external active scan is blocked without state authority), 0 requests for the offline plan |
29
+ | Version consistency | `pyproject.toml`, `core.__version__`, CLI, HTML and JSON reports and the HTTP User-Agent say 2.7.0. The Windows build and verification scripts, the Windows workflow's ZIP and artifact names and the packaged README and launcher derive it through `scripts/source_version.py`; the publishing workflow's manual choices include v2.7.0 |
30
+ | Windows x64 portable | Built from `9fc560c` (clean tree) in run 42918318200 on Windows Server 2025 with Python 3.14.5 and PyInstaller 6.22.3: `GrantTrace-v2.7.0-Windows-x64.zip`, 10,747,513 bytes, SHA256 `a05d6288b85577e6e4bee1a12c9910c8922e564cfa0cf475f96e180832bc1a90`. `scripts/verify_windows_portable.py` passed: extraction to paths with spaces and Chinese characters outside the checkout, no Python on the child PATH, `--version` and `--help`, active and read-only demos, an offline YAML plan with 0 requests, `Start-Demo.bat`, the loopback port closed and no package process left. The artifact host was not reachable from this environment, so the ZIP was not downloaded and rehashed here, and no one double-clicked it on a clean Windows machine |
31
+ | 2.7.0 detectors | Read-only POST, role matrix and sensitive-field checks are covered by `tests/test_v27_hardening.py`, including real HTTP to a loopback server. VAmPI and crAPI were not re-run for this candidate; the read-only GET/HEAD decisions are unchanged (a 200,000-endpoint differential against 2.6.0 found no difference) |
32
+ | Secrets | A pattern scan of all 194 tracked files found no keys, tokens or JWTs; the only match is a dummy private-key marker in a redaction test. GitHub secret scanning is not enabled for the repository |
33
+ | Documentation | Relative links, anchors and links to this repository: 0 problems. `SECURITY.md` describes when a read-only POST is sent; release-specific pages updated in #90 |
34
+
35
+ Local candidate hashes, built from `750a801` in the container:
36
+
37
+ ```text
38
+ 8efe69a82a953310156424bfb2451f6bee842d2e4655445f25daacddba9fe303 granttrace-2.7.0-py3-none-any.whl
39
+ 2c72b227652da135275c17036f58232e051efa53f325aa24ca7ea60c7f183b92 granttrace-2.7.0.tar.gz
40
+ ```
41
+
42
+ The publishing workflow builds the uploaded distributions from the tag. The
43
+ Release's Windows ZIP and `SHA256SUMS.txt` come from the Windows workflow run on
44
+ the release commit itself; their hashes differ from the build above because the
45
+ build records its source commit.
46
+
47
+ Still to do: CI on the release commit, then the tag, the GitHub Release with the
48
+ Windows assets, PyPI publishing and the post-release checks in the
49
+ [release checklist](docs/release-checklist.md), including a clean Windows machine.
50
+
6
51
  ## v2.6.0 pre-release acceptance - 2026-11-08
7
52
 
8
53
  Status: **accepted for release; v2.6.0 is not published.** Release candidate:
@@ -671,6 +671,7 @@ def _export_reports(args: argparse.Namespace, auditor: APISentinelAuditor, spec_
671
671
  )
672
672
  report_target = ("[BOPLA TARGET OMITTED]" if auditor.bopla_config else
673
673
  sanitize_url(args.target, secret_values=auditor.secret_values()))
674
+ report_rbac = auditor.rbac_matrix_record()
674
675
  SecurityReportGenerator.generate(
675
676
  stats=report_stats,
676
677
  findings=report_findings,
@@ -679,23 +680,27 @@ def _export_reports(args: argparse.Namespace, auditor: APISentinelAuditor, spec_
679
680
  output_path=args.output,
680
681
  reproduction_templates=reproduction_templates,
681
682
  secret_values=auditor.secret_values(),
683
+ rbac_matrix=report_rbac,
682
684
  )
683
685
  print(f"[OK] HTML report: {args.output}")
684
686
 
685
687
  if args.export_json:
686
688
  _ensure_parent(args.export_json)
687
689
  with open(args.export_json, "w", encoding="utf-8") as handle:
690
+ json_payload = {
691
+ "tool_version": __version__,
692
+ "report_schema_version": 2,
693
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
694
+ "elapsed_seconds": round(elapsed, 3),
695
+ "target": report_target,
696
+ "stats": report_stats,
697
+ "results": report_results,
698
+ "findings": report_findings,
699
+ }
700
+ if report_rbac:
701
+ json_payload["rbac_matrix"] = report_rbac
688
702
  json.dump(
689
- {
690
- "tool_version": __version__,
691
- "report_schema_version": 2,
692
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
693
- "elapsed_seconds": round(elapsed, 3),
694
- "target": report_target,
695
- "stats": report_stats,
696
- "results": report_results,
697
- "findings": report_findings,
698
- },
703
+ json_payload,
699
704
  handle,
700
705
  indent=2,
701
706
  ensure_ascii=False,
@@ -28,6 +28,17 @@
28
28
  },
29
29
  "anonymous": {
30
30
  "$ref": "#/definitions/anonymous_identity"
31
+ },
32
+ "roles": {
33
+ "type": "object",
34
+ "description": "Additional named roles for the RBAC matrix. Names must not reuse owner, visitor, anonymous or roles; probes run only for operations with an explicit required_role.",
35
+ "propertyNames": {
36
+ "pattern": "^[a-zA-Z0-9_.-]+$",
37
+ "not": {"enum": ["owner", "visitor", "anonymous", "roles"]}
38
+ },
39
+ "additionalProperties": {
40
+ "$ref": "#/definitions/role_identity"
41
+ }
31
42
  }
32
43
  }
33
44
  },
@@ -72,6 +83,39 @@
72
83
  }
73
84
  },
74
85
  "definitions": {
86
+ "role_identity": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "properties": {
90
+ "id": {
91
+ "type": ["string", "integer", "null"],
92
+ "description": "Resource identifier associated with this role identity"
93
+ },
94
+ "role": {
95
+ "type": ["string", "null"],
96
+ "description": "Role compared with required_role (defaults to the role's key)"
97
+ },
98
+ "token": {
99
+ "type": ["string", "null"],
100
+ "description": "Bearer token or Authorization header value for this role"
101
+ },
102
+ "headers": {
103
+ "type": "object",
104
+ "description": "Arbitrary authentication or session headers for this role",
105
+ "additionalProperties": {
106
+ "type": "string"
107
+ }
108
+ },
109
+ "parameters": {
110
+ "type": "object",
111
+ "description": "Role-bound parameters"
112
+ },
113
+ "body": {
114
+ "description": "Request body this role sends to a read-only POST instead of the Owner body; ignored for GET",
115
+ "type": ["object", "array", "string", "number", "null"]
116
+ }
117
+ }
118
+ },
75
119
  "authenticated_identity": {
76
120
  "type": "object",
77
121
  "additionalProperties": false,
@@ -379,6 +423,28 @@
379
423
  "type": "string",
380
424
  "minLength": 1,
381
425
  "description": "Operator-verified role of the configured Visitor identity. Role comparison is exact, without inferring a role hierarchy."
426
+ },
427
+ "read_only": {
428
+ "type": "boolean",
429
+ "description": "POST only: declares the operation side-effect free so the BOLA audit may send it. Its path, operationId or summary must also name a read action (search, query, lookup...) and must not name a mutation."
430
+ },
431
+ "body": {
432
+ "description": "POST only: literal JSON request body sent unchanged by Owner, Visitor and Anonymous. It is never reinterpreted as a per-identity map.",
433
+ "type": ["object", "array", "string", "number"]
434
+ },
435
+ "identity_bodies": {
436
+ "type": "object",
437
+ "description": "POST only, instead of body: 'owner' selects the Owner's object and is also sent by the Visitor's cross request and Anonymous; optional 'visitor_self' selects the Visitor's own object.",
438
+ "additionalProperties": false,
439
+ "required": ["owner"],
440
+ "properties": {
441
+ "owner": {"type": ["object", "array", "string", "number"]},
442
+ "visitor_self": {"type": ["object", "array", "string", "number"]}
443
+ }
444
+ },
445
+ "detect_sensitive_exposure": {
446
+ "type": "boolean",
447
+ "description": "Heuristic lead for unmasked sensitive-named Owner fields returned to the Visitor (OWASP API3). CONFIRMED only with expected_visitor_access 'deny', a confirmed object-level decision and a value that differs from the Visitor's own record; otherwise SUSPICIOUS."
382
448
  }
383
449
  }
384
450
  }
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """GrantTrace core package."""
3
3
 
4
- __version__ = "2.6.0"
4
+ __version__ = "2.7.0"