| { |
| "generated_utc": "2026-09-16T07:59:24.115329+00:00", |
| "stack": "odoo", |
| "subject": "the 8 python.* rules composed into the Odoo stack", |
| "checkouts": { |
| "OpenSPP2-19.0": "cfa0dc8e", |
| "server-tools": "60cd54e5e", |
| "account-financial-tools": "e4c1b86aa" |
| }, |
| "lines": { |
| "OpenSPP2-19.0": 226937, |
| "OCA": 33960 |
| }, |
| "totals": { |
| "true": 9, |
| "false": 42 |
| }, |
| "by_rule": { |
| "python.assert-is-not-validation": { |
| "true": 6, |
| "false": 4 |
| }, |
| "python.decimal-for-money-not-float": { |
| "true": 0, |
| "false": 3 |
| }, |
| "python.eval-and-pickle-execute-their-input": { |
| "true": 0, |
| "false": 1 |
| }, |
| "python.naive-and-aware-datetimes-do-not-compare": { |
| "true": 3, |
| "false": 30 |
| }, |
| "python.path-join-does-not-contain-a-path": { |
| "true": 0, |
| "false": 4 |
| } |
| }, |
| "classes": { |
| "validates-caller-data": 6, |
| "internal-invariant": 3, |
| "odoo-monetary-is-float": 3, |
| "matched-inside-a-docstring": 2, |
| "display-only": 8, |
| "naive-utc-across-an-api-boundary": 2, |
| "utc-by-odoo": 15, |
| "default-never-reached": 2, |
| "self-consistent-utc": 2, |
| "no-user-supplied-component": 4, |
| "re-enforced-on-the-next-line": 1, |
| "created-a-day-ahead": 1, |
| "server-date-as-a-user-date": 1, |
| "self-consistent-local": 1 |
| }, |
| "hits": [ |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OpenSPP2-19.0", |
| "path": "endpoint_route_handler/registry.py", |
| "line": 314, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "options come back off a jsonb row in from_row(), so this asserts about stored data rather than a program invariant; under -O a malformed handler dict is accepted silently", |
| "evidence": "assert \"klass_dotted_path\" in value[\"handler\"]" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OpenSPP2-19.0", |
| "path": "endpoint_route_handler/registry.py", |
| "line": 315, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "same setter, same stored data, second key", |
| "evidence": "assert \"method_name\" in value[\"handler\"]" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_registry/models/reg_relationship.py", |
| "line": 165, |
| "verdict": "false", |
| "class": "internal-invariant", |
| "why": "_check_partner is called from two @api.constrains with the literal 'source' and 'destination'; the assert is impossible unless the module itself is broken, which is what assert is for", |
| "evidence": "assert side in [\"source\", \"destination\"]" |
| }, |
| { |
| "rule": "python.decimal-for-money-not-float", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_programs/models/cel/entitlement_amount_cel.py", |
| "line": 300, |
| "verdict": "false", |
| "class": "odoo-monetary-is-float", |
| "why": "self.amount is a Monetary field, which the ORM defines AS a float; the rule's remedy (Decimal) is not the Odoo idiom and float_round/float_compare are", |
| "evidence": "\"base_amount\": float(self.amount or 0.0)," |
| }, |
| { |
| "rule": "python.decimal-for-money-not-float", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_programs/models/cel/entitlement_amount_cel.py", |
| "line": 316, |
| "verdict": "false", |
| "class": "odoo-monetary-is-float", |
| "why": "the CEL result is coerced for a Monetary field; same reason", |
| "evidence": "amount = float(result)" |
| }, |
| { |
| "rule": "python.eval-and-pickle-execute-their-input", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_cel_domain/models/cel_service.py", |
| "line": 417, |
| "verdict": "false", |
| "class": "matched-inside-a-docstring", |
| "why": "the text is prose in a method docstring -- 'more robust and secure than regex-based conversion to Python eval()' -- and there is no eval call here at all", |
| "evidence": "and secure than regex-based conversion to Python eval()." |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "scripts/compliance/checker.py", |
| "line": 745, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "a Generated: line in a markdown report; formatted once, never compared", |
| "evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\"," |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "scripts/compliance/checker.py", |
| "line": 813, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "the same timestamp in the JSON version of that report", |
| "evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat()," |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_analytics/models/service_aggregation.py", |
| "line": 97, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "computed_at in a result dict that is returned, not stored or compared", |
| "evidence": "\"computed_at\": datetime.now().isoformat()," |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/routers/consent.py", |
| "line": 178, |
| "verdict": "true", |
| "class": "naive-utc-across-an-api-boundary", |
| "why": "datetime.utcnow() is naive, is returned in a ConsentRevokeResponse, and serialises with no offset, so the consumer cannot tell the zone; deprecated in 3.12 for this reason", |
| "evidence": "revoked_at=datetime.utcnow()," |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/routers/consent.py", |
| "line": 270, |
| "verdict": "true", |
| "class": "naive-utc-across-an-api-boundary", |
| "why": "the consent receipt timestamp, same construct. SUPPRESSED, and wrongly: the marker inside that return statement reads 'nosemgrep: odoo-sudo-without-context' and is about a different rule", |
| "evidence": "timestamp=datetime.utcnow(),", |
| "suppressed": true |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/routers/oauth.py", |
| "line": 217, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "utcnow() and now() are the same value inside Odoo, and PyJWT reads a naive exp/iat as UTC, so the token lifetime is right. inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.utcnow()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/services/group_service.py", |
| "line": 142, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/services/group_service.py", |
| "line": 748, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "ended_datetime = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/services/group_service.py", |
| "line": 886, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_api_v2/services/group_service.py", |
| "line": 1100, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_attendance/controllers/controllers.py", |
| "line": 198, |
| "verdict": "false", |
| "class": "default-never-reached", |
| "why": "the server-date default for submitted_datetime sits on the line after check_required_fields rejects a request without it, so it is never used. Re-read 2026-09-26; was classified true.", |
| "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_attendance/controllers/controllers.py", |
| "line": 351, |
| "verdict": "false", |
| "class": "default-never-reached", |
| "why": "the same unreachable default on the second endpoint. Re-read 2026-09-26; was classified true.", |
| "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_audit/models/spp_audit_backend.py", |
| "line": 198, |
| "verdict": "false", |
| "class": "self-consistent-utc", |
| "why": "utcnow() names an audit file and the rotation check compares it against a value from the same call; nothing aware is ever involved", |
| "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_audit/models/spp_audit_backend.py", |
| "line": 216, |
| "verdict": "false", |
| "class": "self-consistent-utc", |
| "why": "the same value, in the rotation check itself", |
| "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_cel_domain/services/cel_functions.py", |
| "line": 92, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "return datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_cel_domain/services/cel_parser.py", |
| "line": 757, |
| "verdict": "false", |
| "class": "matched-inside-a-docstring", |
| "why": "a doctest example inside the docstring of hours_since, not code", |
| "evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_cel_domain/services/cel_parser.py", |
| "line": 766, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "delta = datetime.now() - datetime_value", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_dci_server/middleware/rate_limit.py", |
| "line": 52, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_encryption/models/encryption_provider.py", |
| "line": 110, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\"", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_farmer_registry_demo/models/seeded_farm_generator.py", |
| "line": 1127, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "enrollment_dt = datetime.datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_grm_demo/models/generate_tickets.py", |
| "line": 922, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_mis_demo_v2/models/mis_demo_generator.py", |
| "line": 3388, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "strftime('%Y-%m') builds a period label", |
| "evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_mis_demo_v2/models/mis_demo_generator.py", |
| "line": 3390, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "the same label for the previous three months", |
| "evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_mis_demo_v2/models/mis_demo_generator.py", |
| "line": 3740, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "now = datetime.datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_mis_demo_v2/models/seeded_volume_generator.py", |
| "line": 406, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "enrollment_dt = datetime.datetime.now()", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_programs/models/managers/program_manager.py", |
| "line": 141, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)", |
| "was": "true" |
| }, |
| { |
| "rule": "python.path-join-does-not-contain-a-path", |
| "tree": "OpenSPP2-19.0", |
| "path": "spp_drims_sl_demo/wizard/drims_demo_generator.py", |
| "line": 486, |
| "verdict": "false", |
| "class": "no-user-supplied-component", |
| "why": "file_config['filename'] comes from a list of literals declared in the same function; there is nothing here a caller can point out of the directory", |
| "evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py", |
| "line": 265, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "confirm_order() is a public method reachable over XML-RPC; under -O the operation-type guard is gone and the wrong record is confirmed", |
| "evidence": "assert self.operation_type == \"order\", \"Wrong operation type\"" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py", |
| "line": 27, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "the assert is about self._context, which the client supplies; under -O default_get proceeds with whatever active_model it was given", |
| "evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\"" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py", |
| "line": 289, |
| "verdict": "false", |
| "class": "internal-invariant", |
| "why": "sign is set by this module a few lines up and can only be 1 or -1 unless the module is broken", |
| "evidence": "assert sign in (1, -1)" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py", |
| "line": 117, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "self.overwrite is a text field a user types; the assert is the type check on literal_eval's result, and the except clause catches AssertionError explicitly -- this IS the validation, and under -O it disappears and .keys() raises AttributeError instead of the ValidationError the code means to raise", |
| "evidence": "assert isinstance(overwrite_vals, dict)" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "server-tools/base_time_parameter/models/base.py", |
| "line": 19, |
| "verdict": "true", |
| "class": "validates-caller-data", |
| "why": "get_time_parameter is public and date arrives from the caller, possibly resolved off a field by name; under -O a wrong type reaches the search", |
| "evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\"" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "server-tools/jsonifier/models/ir_exports_resolver.py", |
| "line": 42, |
| "verdict": "false", |
| "class": "re-enforced-on-the-next-line", |
| "why": "the assert guards a zip(..., strict=True) one line below, which raises ValueError on the same mismatch; removing the assert changes the exception type and nothing else", |
| "evidence": "assert len(param) == len(records)" |
| }, |
| { |
| "rule": "python.assert-is-not-validation", |
| "tree": "OCA", |
| "path": "server-tools/jsonifier/models/utils.py", |
| "line": 3, |
| "verdict": "false", |
| "class": "internal-invariant", |
| "why": "a module-private helper's contract with its only callers", |
| "evidence": "assert isinstance(parser, list)" |
| }, |
| { |
| "rule": "python.decimal-for-money-not-float", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py", |
| "line": 110, |
| "verdict": "false", |
| "class": "odoo-monetary-is-float", |
| "why": "a spreadsheet cell becomes an accounting amount on a Monetary field, which the ORM defines as float", |
| "evidence": "amount = float(raw_value)" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py", |
| "line": 23, |
| "verdict": "false", |
| "class": "created-a-day-ahead", |
| "why": "the cron creates the next fiscal year while the last still has a day to run (date_to < today + 1 day), which is further ahead than any offset between UTC and the company's calendar. Re-read 2026-09-26; was classified true.", |
| "evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py", |
| "line": 45, |
| "verdict": "true", |
| "class": "server-date-as-a-user-date", |
| "why": "an accounting move's date defaults to the server's UTC date. For a company ahead of UTC, a write-off made in its early hours is dated the day before, and at month end in the period before. fields.Date.context_today is the idiom. Stands after the 2026-09-26 re-read, under a class that does not depend on the server's clock.", |
| "evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "server-tools/auditlog/models/autovacuum.py", |
| "line": 25, |
| "verdict": "false", |
| "class": "utc-by-odoo", |
| "why": "inside Odoo this 'local' time is UTC: import odoo pins the process to TZ=UTC (odoo/_monkeypatches, time.tzset), and fields.Datetime.now() is itself datetime.now().replace(microsecond=0). The server offset this verdict rested on cannot occur in an Odoo process. Re-read 2026-09-26; was classified true.", |
| "evidence": "deadline = datetime.now() - timedelta(days=days)", |
| "was": "true" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "server-tools/auto_backup/models/db_backup.py", |
| "line": 146, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "the timestamp names a backup file", |
| "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "server-tools/auto_backup/models/db_backup.py", |
| "line": 171, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "the same, on the sftp path", |
| "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "server-tools/auto_backup/models/db_backup.py", |
| "line": 218, |
| "verdict": "false", |
| "class": "self-consistent-local", |
| "why": "cleanup() builds the oldest filename from the same local clock the filenames were written with, so the two agree", |
| "evidence": "now = datetime.now()" |
| }, |
| { |
| "rule": "python.naive-and-aware-datetimes-do-not-compare", |
| "tree": "OCA", |
| "path": "server-tools/excel_import_export/models/xlsx_export.py", |
| "line": 268, |
| "verdict": "false", |
| "class": "display-only", |
| "why": "a temp filename uniquifier", |
| "evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]" |
| }, |
| { |
| "rule": "python.path-join-does-not-contain-a-path", |
| "tree": "OCA", |
| "path": "server-tools/auto_backup/models/db_backup.py", |
| "line": 154, |
| "verdict": "false", |
| "class": "no-user-supplied-component", |
| "why": "rec.folder is configuration and filename is generated from a timestamp; neither is a path a caller can traverse with", |
| "evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:" |
| }, |
| { |
| "rule": "python.path-join-does-not-contain-a-path", |
| "tree": "OCA", |
| "path": "server-tools/auto_backup/models/db_backup.py", |
| "line": 186, |
| "verdict": "false", |
| "class": "no-user-supplied-component", |
| "why": "the same join on the sftp path", |
| "evidence": "os.path.join(rec.folder, filename), \"wb\"" |
| }, |
| { |
| "rule": "python.path-join-does-not-contain-a-path", |
| "tree": "OCA", |
| "path": "server-tools/upgrade_analysis/models/upgrade_analysis.py", |
| "line": 106, |
| "verdict": "false", |
| "class": "no-user-supplied-component", |
| "why": "full_path is built from get_module_path and filename is internal", |
| "evidence": "logfile = os.path.join(full_path, filename)" |
| } |
| ], |
| "note": "Every finding the composition ADDS to an Odoo review, read by hand in its own file. These 8 rules were previously unreachable from the odoo stack: selection was strict equality on the stack field, so an Odoo module got the 19 ORM rules and none of these.\n\nThe verdict test for the datetime rule, which is 32 of the 50: an Odoo Datetime field is naive UTC by contract, so a naive LOCAL datetime that reaches one -- written to it, compared against it, or handed to something that does -- is a true positive, and one that is only formatted for display or used to name a file is not. Note what that makes these: latent on a server clocked to UTC, wrong by the offset on any other. OpenSPP's own history carries two fixes of exactly this class, which is why it is graded true rather than theoretical.\n\nThree rules fired and were never right: decimal-for-money-not-float (0 of 3), path-join-does-not-contain-a-path (0 of 4), and eval-and-pickle-execute-their-input (0 of 1). The first has a reason rather than a sample size behind it -- Odoo's Monetary field IS a float, so the rule's remedy is not the Odoo idiom -- and the other two have four findings between them, which is not enough to conclude anything about a checker. All three are named in the README rather than quietly dropped.\n\nThree more never fired at all on either tree: mutable-default-argument, bare-except-catches-the-exit and subprocess-shell-true-is-injection. Silence is not a pass.\n\nTwo of the 50 are the same checker defect: the pattern scanner strips # comments before matching and does not strip docstrings, so `eval()` named in prose and a `datetime.now()` inside a doctest both read as code. That is a fault in scan_source, not in either rule.\n\nOne finding is suppressed and should not be. The consent-receipt timestamp sits inside a return statement that carries a `# nosemgrep: odoo-sudo-without-context` marker four lines below it, and suppression reads any marker in the statement without asking which rule it names. It is counted apart here, as fieldtest counts it.\n\nSince 210b09e a marker with words in its code covers only rules sharing one of those words, so a sudo marker no longer speaks for this datetime finding: it is reported, and the totals above now count it with the rest. 27 true of 51, 21 of them the datetime rule.\n\n2026-09-26: 18 of the datetime rule's 21 true verdicts reversed. They rested on a naive LOCAL datetime differing from the ORM's naive UTC by the server's offset, and an Odoo process has no offset: import odoo sets TZ=UTC, and fields.Datetime.now() is datetime.now() itself. Three stand: two naive UTC timestamps leaving through an API with no offset, and one server date used as a company's calendar day. Each reversed row keeps was: true." |
| } |
|
|