diff --git a/.github/governance/execution-profiles.yaml b/.github/governance/execution-profiles.yaml new file mode 100644 index 0000000..2d58873 --- /dev/null +++ b/.github/governance/execution-profiles.yaml @@ -0,0 +1,45 @@ +{ + "schema": "l9.execution-profiles/v1", + "profiles": { + "pr_fast": { + "sdk_profile": "ci_fast", + "strict": true, + "default_mode": "blocking", + "providers": ["semgrep"], + "policy": "", + "allowed_events": ["pull_request", "workflow_dispatch"] + }, + "merge": { + "sdk_profile": "ci_fast", + "strict": true, + "default_mode": "blocking", + "providers": ["semgrep"], + "policy": "", + "allowed_events": ["push", "workflow_dispatch"] + }, + "nightly": { + "sdk_profile": "ci_deep", + "strict": true, + "default_mode": "advisory", + "providers": ["semgrep"], + "policy": "", + "allowed_events": ["schedule", "workflow_dispatch"] + }, + "release": { + "sdk_profile": "ci_deep", + "strict": true, + "default_mode": "blocking", + "providers": ["semgrep"], + "policy": "", + "allowed_events": ["push", "workflow_dispatch"] + }, + "supply_chain": { + "sdk_profile": "ci_deep", + "strict": true, + "default_mode": "blocking", + "providers": ["semgrep"], + "policy": "", + "allowed_events": ["schedule", "workflow_dispatch"] + } + } +} diff --git a/.github/governance/promotion-policy.yaml b/.github/governance/promotion-policy.yaml new file mode 100644 index 0000000..69b5470 --- /dev/null +++ b/.github/governance/promotion-policy.yaml @@ -0,0 +1,16 @@ +{ + "schema": "l9.promotion-policy/v1", + "transitions": { + "disabled": ["shadow"], + "shadow": ["advisory", "disabled"], + "advisory": ["blocking", "shadow"], + "blocking": ["advisory"] + }, + "requirements": { + "minimum_observation_runs": 20, + "minimum_observation_days": 7, + "maximum_contract_failures": 0, + "maximum_artifact_validation_failures": 0, + "approval_required": true + } +} diff --git a/.github/governance/provider-requiredness.yaml b/.github/governance/provider-requiredness.yaml new file mode 100644 index 0000000..a8194e4 --- /dev/null +++ b/.github/governance/provider-requiredness.yaml @@ -0,0 +1,20 @@ +{ + "schema": "l9.provider-requiredness/v1", + "profiles": { + "pr_fast": { + "semgrep": true + }, + "merge": { + "semgrep": true + }, + "nightly": { + "semgrep": false + }, + "release": { + "semgrep": true + }, + "supply_chain": { + "semgrep": true + } + } +} diff --git a/.github/governance/quality-thresholds.yaml b/.github/governance/quality-thresholds.yaml new file mode 100644 index 0000000..4e4e100 --- /dev/null +++ b/.github/governance/quality-thresholds.yaml @@ -0,0 +1,21 @@ +{ + "schema": "l9.quality-threshold-selection/v1", + "profiles": { + "pr_fast": { + "sdk_policy": ".github/governance/semgrep-policy.yaml" + }, + "merge": { + "sdk_policy": ".github/governance/semgrep-policy.yaml" + }, + "nightly": { + "sdk_policy": ".github/governance/semgrep-policy.yaml" + }, + "release": { + "sdk_policy": ".github/governance/semgrep-policy.yaml" + }, + "supply_chain": { + "sdk_policy": ".github/governance/semgrep-policy.yaml" + } + }, + "note": "Core selects an SDK policy file but never evaluates finding thresholds itself. Point sdk_policy at a policy file the pinned SDK understands to raise/lower gates." +} diff --git a/.github/governance/rule-modes.yaml b/.github/governance/rule-modes.yaml new file mode 100644 index 0000000..443e424 --- /dev/null +++ b/.github/governance/rule-modes.yaml @@ -0,0 +1,19 @@ +{ + "schema": "l9.rule-modes/v1", + "defaults": { + "pr_fast": "blocking", + "merge": "blocking", + "nightly": "advisory", + "release": "blocking", + "supply_chain": "blocking" + }, + "provider_overrides": { + "semgrep": {} + }, + "allowed_modes": [ + "blocking", + "advisory", + "shadow", + "disabled" + ] +} diff --git a/.github/governance/semgrep-identity-map.yaml b/.github/governance/semgrep-identity-map.yaml new file mode 100644 index 0000000..3c02383 --- /dev/null +++ b/.github/governance/semgrep-identity-map.yaml @@ -0,0 +1,463 @@ +{ + "schema": "l9.identity-map/v1", + "metadata": { + "provider_id": "semgrep", + "version": "1" + }, + "rules": { + "python.aws-lambda.security.dangerous-asyncio-create-exec.dangerous-asyncio-create-exec": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-asyncio-create-exec.dangerous-asyncio-create-exec" + }, + "python.aws-lambda.security.dangerous-asyncio-exec.dangerous-asyncio-exec": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-asyncio-exec.dangerous-asyncio-exec" + }, + "python.aws-lambda.security.dangerous-asyncio-shell.dangerous-asyncio-shell": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-asyncio-shell.dangerous-asyncio-shell" + }, + "python.aws-lambda.security.dangerous-spawn-process.dangerous-spawn-process": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-spawn-process.dangerous-spawn-process" + }, + "python.aws-lambda.security.dangerous-subprocess-use.dangerous-subprocess-use": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-subprocess-use.dangerous-subprocess-use" + }, + "python.aws-lambda.security.dangerous-system-call.dangerous-system-call": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dangerous-system-call.dangerous-system-call" + }, + "python.aws-lambda.security.dynamodb-filter-injection.dynamodb-filter-injection": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.dynamodb-filter-injection.dynamodb-filter-injection" + }, + "python.aws-lambda.security.mysql-sqli.mysql-sqli": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.mysql-sqli.mysql-sqli" + }, + "python.aws-lambda.security.psycopg-sqli.psycopg-sqli": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.psycopg-sqli.psycopg-sqli" + }, + "python.aws-lambda.security.pymssql-sqli.pymssql-sqli": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.pymssql-sqli.pymssql-sqli" + }, + "python.aws-lambda.security.pymysql-sqli.pymysql-sqli": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.pymysql-sqli.pymysql-sqli" + }, + "python.aws-lambda.security.sqlalchemy-sqli.sqlalchemy-sqli": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.sqlalchemy-sqli.sqlalchemy-sqli" + }, + "python.aws-lambda.security.tainted-code-exec.tainted-code-exec": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.tainted-code-exec.tainted-code-exec" + }, + "python.aws-lambda.security.tainted-html-response.tainted-html-response": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.tainted-html-response.tainted-html-response" + }, + "python.aws-lambda.security.tainted-html-string.tainted-html-string": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.tainted-html-string.tainted-html-string" + }, + "python.aws-lambda.security.tainted-pickle-deserialization.tainted-pickle-deserialization": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.tainted-pickle-deserialization.tainted-pickle-deserialization" + }, + "python.aws-lambda.security.tainted-sql-string.tainted-sql-string": { + "canonical_rule_id": "l9:semgrep:python.aws-lambda.security.tainted-sql-string.tainted-sql-string" + }, + "python.boto3.security.hardcoded-token.hardcoded-token": { + "canonical_rule_id": "l9:semgrep:python.boto3.security.hardcoded-token.hardcoded-token" + }, + "python.cryptography.security.empty-aes-key.empty-aes-key": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.empty-aes-key.empty-aes-key" + }, + "python.cryptography.security.insecure-cipher-algorithms-arc4.insecure-cipher-algorithm-arc4": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-cipher-algorithms-arc4.insecure-cipher-algorithm-arc4" + }, + "python.cryptography.security.insecure-cipher-algorithms-blowfish.insecure-cipher-algorithm-blowfish": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-cipher-algorithms-blowfish.insecure-cipher-algorithm-blowfish" + }, + "python.cryptography.security.insecure-cipher-algorithms.insecure-cipher-algorithm-idea": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-cipher-algorithms.insecure-cipher-algorithm-idea" + }, + "python.cryptography.security.insecure-cipher-mode-ecb.insecure-cipher-mode-ecb": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-cipher-mode-ecb.insecure-cipher-mode-ecb" + }, + "python.cryptography.security.insecure-hash-algorithms-md5.insecure-hash-algorithm-md5": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-hash-algorithms-md5.insecure-hash-algorithm-md5" + }, + "python.cryptography.security.insecure-hash-algorithms.insecure-hash-algorithm-sha1": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insecure-hash-algorithms.insecure-hash-algorithm-sha1" + }, + "python.cryptography.security.insufficient-dsa-key-size.insufficient-dsa-key-size": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insufficient-dsa-key-size.insufficient-dsa-key-size" + }, + "python.cryptography.security.insufficient-ec-key-size.insufficient-ec-key-size": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insufficient-ec-key-size.insufficient-ec-key-size" + }, + "python.cryptography.security.insufficient-rsa-key-size.insufficient-rsa-key-size": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.insufficient-rsa-key-size.insufficient-rsa-key-size" + }, + "python.cryptography.security.mode-without-authentication.crypto-mode-without-authentication": { + "canonical_rule_id": "l9:semgrep:python.cryptography.security.mode-without-authentication.crypto-mode-without-authentication" + }, + "python.distributed.security.require-encryption": { + "canonical_rule_id": "l9:semgrep:python.distributed.security.require-encryption" + }, + "python.django.security.audit.avoid-insecure-deserialization.avoid-insecure-deserialization": { + "canonical_rule_id": "l9:semgrep:python.django.security.audit.avoid-insecure-deserialization.avoid-insecure-deserialization" + }, + "python.django.security.django-using-request-post-after-is-valid.django-using-request-post-after-is-valid": { + "canonical_rule_id": "l9:semgrep:python.django.security.django-using-request-post-after-is-valid.django-using-request-post-after-is-valid" + }, + "python.django.security.hashids-with-django-secret.hashids-with-django-secret": { + "canonical_rule_id": "l9:semgrep:python.django.security.hashids-with-django-secret.hashids-with-django-secret" + }, + "python.django.security.injection.code.user-eval-format-string.user-eval-format-string": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.code.user-eval-format-string.user-eval-format-string" + }, + "python.django.security.injection.code.user-eval.user-eval": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.code.user-eval.user-eval" + }, + "python.django.security.injection.code.user-exec-format-string.user-exec-format-string": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.code.user-exec-format-string.user-exec-format-string" + }, + "python.django.security.injection.code.user-exec.user-exec": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.code.user-exec.user-exec" + }, + "python.django.security.injection.command.command-injection-os-system.command-injection-os-system": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.command.command-injection-os-system.command-injection-os-system" + }, + "python.django.security.injection.command.subprocess-injection.subprocess-injection": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.command.subprocess-injection.subprocess-injection" + }, + "python.django.security.injection.csv-writer-injection.csv-writer-injection": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.csv-writer-injection.csv-writer-injection" + }, + "python.django.security.injection.email.xss-html-email-body.xss-html-email-body": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.email.xss-html-email-body.xss-html-email-body" + }, + "python.django.security.injection.email.xss-send-mail-html-message.xss-send-mail-html-message": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.email.xss-send-mail-html-message.xss-send-mail-html-message" + }, + "python.django.security.injection.open-redirect.open-redirect": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.open-redirect.open-redirect" + }, + "python.django.security.injection.path-traversal.path-traversal-open.path-traversal-open": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.path-traversal.path-traversal-open.path-traversal-open" + }, + "python.django.security.injection.raw-html-format.raw-html-format": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.raw-html-format.raw-html-format" + }, + "python.django.security.injection.reflected-data-httpresponse.reflected-data-httpresponse": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.reflected-data-httpresponse.reflected-data-httpresponse" + }, + "python.django.security.injection.reflected-data-httpresponsebadrequest.reflected-data-httpresponsebadrequest": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.reflected-data-httpresponsebadrequest.reflected-data-httpresponsebadrequest" + }, + "python.django.security.injection.request-data-fileresponse.request-data-fileresponse": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.request-data-fileresponse.request-data-fileresponse" + }, + "python.django.security.injection.request-data-write.request-data-write": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.request-data-write.request-data-write" + }, + "python.django.security.injection.sql.sql-injection-extra.sql-injection-using-extra-where": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.sql.sql-injection-extra.sql-injection-using-extra-where" + }, + "python.django.security.injection.sql.sql-injection-rawsql.sql-injection-using-rawsql": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.sql.sql-injection-rawsql.sql-injection-using-rawsql" + }, + "python.django.security.injection.sql.sql-injection-using-db-cursor-execute.sql-injection-db-cursor-execute": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.sql.sql-injection-using-db-cursor-execute.sql-injection-db-cursor-execute" + }, + "python.django.security.injection.sql.sql-injection-using-raw.sql-injection-using-raw": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.sql.sql-injection-using-raw.sql-injection-using-raw" + }, + "python.django.security.injection.ssrf.ssrf-injection-requests.ssrf-injection-requests": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.ssrf.ssrf-injection-requests.ssrf-injection-requests" + }, + "python.django.security.injection.ssrf.ssrf-injection-urllib.ssrf-injection-urllib": { + "canonical_rule_id": "l9:semgrep:python.django.security.injection.ssrf.ssrf-injection-urllib.ssrf-injection-urllib" + }, + "python.django.security.nan-injection.nan-injection": { + "canonical_rule_id": "l9:semgrep:python.django.security.nan-injection.nan-injection" + }, + "python.django.security.passwords.password-empty-string.password-empty-string": { + "canonical_rule_id": "l9:semgrep:python.django.security.passwords.password-empty-string.password-empty-string" + }, + "python.django.security.passwords.use-none-for-password-default.use-none-for-password-default": { + "canonical_rule_id": "l9:semgrep:python.django.security.passwords.use-none-for-password-default.use-none-for-password-default" + }, + "python.fastapi.security.wildcard-cors.wildcard-cors": { + "canonical_rule_id": "l9:semgrep:python.fastapi.security.wildcard-cors.wildcard-cors" + }, + "python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host": { + "canonical_rule_id": "l9:semgrep:python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host" + }, + "python.flask.security.audit.app-run-security-config.avoid_using_app_run_directly": { + "canonical_rule_id": "l9:semgrep:python.flask.security.audit.app-run-security-config.avoid_using_app_run_directly" + }, + "python.flask.security.audit.debug-enabled.debug-enabled": { + "canonical_rule_id": "l9:semgrep:python.flask.security.audit.debug-enabled.debug-enabled" + }, + "python.flask.security.audit.directly-returned-format-string.directly-returned-format-string": { + "canonical_rule_id": "l9:semgrep:python.flask.security.audit.directly-returned-format-string.directly-returned-format-string" + }, + "python.flask.security.audit.flask-url-for-external-true.flask-url-for-external-true": { + "canonical_rule_id": "l9:semgrep:python.flask.security.audit.flask-url-for-external-true.flask-url-for-external-true" + }, + "python.flask.security.hashids-with-flask-secret.hashids-with-flask-secret": { + "canonical_rule_id": "l9:semgrep:python.flask.security.hashids-with-flask-secret.hashids-with-flask-secret" + }, + "python.flask.security.injection.csv-writer-injection.csv-writer-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.csv-writer-injection.csv-writer-injection" + }, + "python.flask.security.injection.nan-injection.nan-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.nan-injection.nan-injection" + }, + "python.flask.security.injection.os-system-injection.os-system-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.os-system-injection.os-system-injection" + }, + "python.flask.security.injection.path-traversal-open.path-traversal-open": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.path-traversal-open.path-traversal-open" + }, + "python.flask.security.injection.raw-html-concat.raw-html-format": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.raw-html-concat.raw-html-format" + }, + "python.flask.security.injection.ssrf-requests.ssrf-requests": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.ssrf-requests.ssrf-requests" + }, + "python.flask.security.injection.subprocess-injection.subprocess-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.subprocess-injection.subprocess-injection" + }, + "python.flask.security.injection.tainted-sql-string.tainted-sql-string": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.tainted-sql-string.tainted-sql-string" + }, + "python.flask.security.injection.tainted-url-host.tainted-url-host": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.tainted-url-host.tainted-url-host" + }, + "python.flask.security.injection.user-eval.eval-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.user-eval.eval-injection" + }, + "python.flask.security.injection.user-exec.exec-injection": { + "canonical_rule_id": "l9:semgrep:python.flask.security.injection.user-exec.exec-injection" + }, + "python.jinja2.security.audit.autoescape-disabled-false.incorrect-autoescape-disabled": { + "canonical_rule_id": "l9:semgrep:python.jinja2.security.audit.autoescape-disabled-false.incorrect-autoescape-disabled" + }, + "python.jinja2.security.audit.missing-autoescape-disabled.missing-autoescape-disabled": { + "canonical_rule_id": "l9:semgrep:python.jinja2.security.audit.missing-autoescape-disabled.missing-autoescape-disabled" + }, + "python.jwt.security.jwt-hardcode.jwt-python-hardcoded-secret": { + "canonical_rule_id": "l9:semgrep:python.jwt.security.jwt-hardcode.jwt-python-hardcoded-secret" + }, + "python.jwt.security.jwt-none-alg.jwt-python-none-alg": { + "canonical_rule_id": "l9:semgrep:python.jwt.security.jwt-none-alg.jwt-python-none-alg" + }, + "python.jwt.security.unverified-jwt-decode.unverified-jwt-decode": { + "canonical_rule_id": "l9:semgrep:python.jwt.security.unverified-jwt-decode.unverified-jwt-decode" + }, + "python.lang.security.audit.dangerous-asyncio-exec-tainted-env-args.dangerous-asyncio-exec-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-asyncio-exec-tainted-env-args.dangerous-asyncio-exec-tainted-env-args" + }, + "python.lang.security.audit.dangerous-asyncio-shell-tainted-env-args.dangerous-asyncio-shell-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-asyncio-shell-tainted-env-args.dangerous-asyncio-shell-tainted-env-args" + }, + "python.lang.security.audit.dangerous-code-run-tainted-env-args.dangerous-interactive-code-run-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-code-run-tainted-env-args.dangerous-interactive-code-run-tainted-env-args" + }, + "python.lang.security.audit.dangerous-os-exec-tainted-env-args.dangerous-os-exec-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-os-exec-tainted-env-args.dangerous-os-exec-tainted-env-args" + }, + "python.lang.security.audit.dangerous-spawn-process-tainted-env-args.dangerous-spawn-process-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-spawn-process-tainted-env-args.dangerous-spawn-process-tainted-env-args" + }, + "python.lang.security.audit.dangerous-subinterpreters-run-string-tainted-env-args.dangerous-subinterpreters-run-string-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-subinterpreters-run-string-tainted-env-args.dangerous-subinterpreters-run-string-tainted-env-args" + }, + "python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args" + }, + "python.lang.security.audit.dangerous-system-call-tainted-env-args.dangerous-system-call-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-system-call-tainted-env-args.dangerous-system-call-tainted-env-args" + }, + "python.lang.security.audit.dangerous-testcapi-run-in-subinterp-tainted-env-args.dangerous-testcapi-run-in-subinterp-tainted-env-args": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.dangerous-testcapi-run-in-subinterp-tainted-env-args.dangerous-testcapi-run-in-subinterp-tainted-env-args" + }, + "python.lang.security.audit.insecure-file-permissions.insecure-file-permissions": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.insecure-file-permissions.insecure-file-permissions" + }, + "python.lang.security.audit.insecure-transport.requests.request-session-http-in-with-context.request-session-http-in-with-context": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.insecure-transport.requests.request-session-http-in-with-context.request-session-http-in-with-context" + }, + "python.lang.security.audit.insecure-transport.requests.request-session-with-http.request-session-with-http": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.insecure-transport.requests.request-session-with-http.request-session-with-http" + }, + "python.lang.security.audit.insecure-transport.requests.request-with-http.request-with-http": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.insecure-transport.requests.request-with-http.request-with-http" + }, + "python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure" + }, + "python.lang.security.audit.md5-used-as-password.md5-used-as-password": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.md5-used-as-password.md5-used-as-password" + }, + "python.lang.security.audit.network.bind.avoid-bind-to-all-interfaces": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.network.bind.avoid-bind-to-all-interfaces" + }, + "python.lang.security.audit.network.disabled-cert-validation.disabled-cert-validation": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.network.disabled-cert-validation.disabled-cert-validation" + }, + "python.lang.security.audit.network.http-not-https-connection.http-not-https-connection": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.network.http-not-https-connection.http-not-https-connection" + }, + "python.lang.security.audit.sha224-hash.sha224-hash": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.sha224-hash.sha224-hash" + }, + "python.lang.security.audit.ssl-wrap-socket-is-deprecated.ssl-wrap-socket-is-deprecated": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.ssl-wrap-socket-is-deprecated.ssl-wrap-socket-is-deprecated" + }, + "python.lang.security.audit.subprocess-shell-true.subprocess-shell-true": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.subprocess-shell-true.subprocess-shell-true" + }, + "python.lang.security.audit.weak-ssl-version.weak-ssl-version": { + "canonical_rule_id": "l9:semgrep:python.lang.security.audit.weak-ssl-version.weak-ssl-version" + }, + "python.lang.security.dangerous-code-run.dangerous-interactive-code-run": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-code-run.dangerous-interactive-code-run" + }, + "python.lang.security.dangerous-os-exec.dangerous-os-exec": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-os-exec.dangerous-os-exec" + }, + "python.lang.security.dangerous-spawn-process.dangerous-spawn-process": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-spawn-process.dangerous-spawn-process" + }, + "python.lang.security.dangerous-subinterpreters-run-string.dangerous-subinterpreters-run-string": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-subinterpreters-run-string.dangerous-subinterpreters-run-string" + }, + "python.lang.security.dangerous-subprocess-use.dangerous-subprocess-use": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-subprocess-use.dangerous-subprocess-use" + }, + "python.lang.security.dangerous-system-call.dangerous-system-call": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-system-call.dangerous-system-call" + }, + "python.lang.security.dangerous-testcapi-run-in-subinterp.dangerous-testcapi-run-in-subinterp": { + "canonical_rule_id": "l9:semgrep:python.lang.security.dangerous-testcapi-run-in-subinterp.dangerous-testcapi-run-in-subinterp" + }, + "python.lang.security.deserialization.avoid-pyyaml-load.avoid-pyyaml-load": { + "canonical_rule_id": "l9:semgrep:python.lang.security.deserialization.avoid-pyyaml-load.avoid-pyyaml-load" + }, + "python.lang.security.deserialization.avoid-unsafe-ruamel.avoid-unsafe-ruamel": { + "canonical_rule_id": "l9:semgrep:python.lang.security.deserialization.avoid-unsafe-ruamel.avoid-unsafe-ruamel" + }, + "python.lang.security.deserialization.pickle.avoid-shelve": { + "canonical_rule_id": "l9:semgrep:python.lang.security.deserialization.pickle.avoid-shelve" + }, + "python.lang.security.insecure-hash-algorithms-md5.insecure-hash-algorithm-md5": { + "canonical_rule_id": "l9:semgrep:python.lang.security.insecure-hash-algorithms-md5.insecure-hash-algorithm-md5" + }, + "python.lang.security.insecure-hash-algorithms.insecure-hash-algorithm-sha1": { + "canonical_rule_id": "l9:semgrep:python.lang.security.insecure-hash-algorithms.insecure-hash-algorithm-sha1" + }, + "python.lang.security.insecure-hash-function.insecure-hash-function": { + "canonical_rule_id": "l9:semgrep:python.lang.security.insecure-hash-function.insecure-hash-function" + }, + "python.lang.security.insecure-uuid-version.insecure-uuid-version": { + "canonical_rule_id": "l9:semgrep:python.lang.security.insecure-uuid-version.insecure-uuid-version" + }, + "python.lang.security.unverified-ssl-context.unverified-ssl-context": { + "canonical_rule_id": "l9:semgrep:python.lang.security.unverified-ssl-context.unverified-ssl-context" + }, + "python.lang.security.use-defused-xml-parse.use-defused-xml-parse": { + "canonical_rule_id": "l9:semgrep:python.lang.security.use-defused-xml-parse.use-defused-xml-parse" + }, + "python.pycryptodome.security.insecure-cipher-algorithm-blowfish.insecure-cipher-algorithm-blowfish": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-cipher-algorithm-blowfish.insecure-cipher-algorithm-blowfish" + }, + "python.pycryptodome.security.insecure-cipher-algorithm-des.insecure-cipher-algorithm-des": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-cipher-algorithm-des.insecure-cipher-algorithm-des" + }, + "python.pycryptodome.security.insecure-cipher-algorithm-rc2.insecure-cipher-algorithm-rc2": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-cipher-algorithm-rc2.insecure-cipher-algorithm-rc2" + }, + "python.pycryptodome.security.insecure-cipher-algorithm-rc4.insecure-cipher-algorithm-rc4": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-cipher-algorithm-rc4.insecure-cipher-algorithm-rc4" + }, + "python.pycryptodome.security.insecure-cipher-algorithm.insecure-cipher-algorithm-xor": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-cipher-algorithm.insecure-cipher-algorithm-xor" + }, + "python.pycryptodome.security.insecure-hash-algorithm-md2.insecure-hash-algorithm-md2": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-hash-algorithm-md2.insecure-hash-algorithm-md2" + }, + "python.pycryptodome.security.insecure-hash-algorithm-md4.insecure-hash-algorithm-md4": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-hash-algorithm-md4.insecure-hash-algorithm-md4" + }, + "python.pycryptodome.security.insecure-hash-algorithm-md5.insecure-hash-algorithm-md5": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-hash-algorithm-md5.insecure-hash-algorithm-md5" + }, + "python.pycryptodome.security.insecure-hash-algorithm.insecure-hash-algorithm-sha1": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insecure-hash-algorithm.insecure-hash-algorithm-sha1" + }, + "python.pycryptodome.security.insufficient-dsa-key-size.insufficient-dsa-key-size": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insufficient-dsa-key-size.insufficient-dsa-key-size" + }, + "python.pycryptodome.security.insufficient-rsa-key-size.insufficient-rsa-key-size": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.insufficient-rsa-key-size.insufficient-rsa-key-size" + }, + "python.pycryptodome.security.mode-without-authentication.crypto-mode-without-authentication": { + "canonical_rule_id": "l9:semgrep:python.pycryptodome.security.mode-without-authentication.crypto-mode-without-authentication" + }, + "python.pymongo.security.mongodb.mongo-client-bad-auth": { + "canonical_rule_id": "l9:semgrep:python.pymongo.security.mongodb.mongo-client-bad-auth" + }, + "python.pyramid.audit.authtkt-cookie-httponly-unsafe-default.pyramid-authtkt-cookie-httponly-unsafe-default": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.authtkt-cookie-httponly-unsafe-default.pyramid-authtkt-cookie-httponly-unsafe-default" + }, + "python.pyramid.audit.authtkt-cookie-httponly-unsafe-value.pyramid-authtkt-cookie-httponly-unsafe-value": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.authtkt-cookie-httponly-unsafe-value.pyramid-authtkt-cookie-httponly-unsafe-value" + }, + "python.pyramid.audit.authtkt-cookie-samesite.pyramid-authtkt-cookie-samesite": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.authtkt-cookie-samesite.pyramid-authtkt-cookie-samesite" + }, + "python.pyramid.audit.authtkt-cookie-secure-unsafe-default.pyramid-authtkt-cookie-secure-unsafe-default": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.authtkt-cookie-secure-unsafe-default.pyramid-authtkt-cookie-secure-unsafe-default" + }, + "python.pyramid.audit.authtkt-cookie-secure-unsafe-value.pyramid-authtkt-cookie-secure-unsafe-value": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.authtkt-cookie-secure-unsafe-value.pyramid-authtkt-cookie-secure-unsafe-value" + }, + "python.pyramid.audit.csrf-origin-check-disabled-globally.pyramid-csrf-origin-check-disabled-globally": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.csrf-origin-check-disabled-globally.pyramid-csrf-origin-check-disabled-globally" + }, + "python.pyramid.audit.csrf-origin-check-disabled.pyramid-csrf-origin-check-disabled": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.csrf-origin-check-disabled.pyramid-csrf-origin-check-disabled" + }, + "python.pyramid.audit.set-cookie-httponly-unsafe-default.pyramid-set-cookie-httponly-unsafe-default": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-httponly-unsafe-default.pyramid-set-cookie-httponly-unsafe-default" + }, + "python.pyramid.audit.set-cookie-httponly-unsafe-value.pyramid-set-cookie-httponly-unsafe-value": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-httponly-unsafe-value.pyramid-set-cookie-httponly-unsafe-value" + }, + "python.pyramid.audit.set-cookie-samesite-unsafe-default.pyramid-set-cookie-samesite-unsafe-default": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-samesite-unsafe-default.pyramid-set-cookie-samesite-unsafe-default" + }, + "python.pyramid.audit.set-cookie-samesite-unsafe-value.pyramid-set-cookie-samesite-unsafe-value": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-samesite-unsafe-value.pyramid-set-cookie-samesite-unsafe-value" + }, + "python.pyramid.audit.set-cookie-secure-unsafe-default.pyramid-set-cookie-secure-unsafe-default": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-secure-unsafe-default.pyramid-set-cookie-secure-unsafe-default" + }, + "python.pyramid.audit.set-cookie-secure-unsafe-value.pyramid-set-cookie-secure-unsafe-value": { + "canonical_rule_id": "l9:semgrep:python.pyramid.audit.set-cookie-secure-unsafe-value.pyramid-set-cookie-secure-unsafe-value" + }, + "python.pyramid.security.csrf-check-disabled-globally.pyramid-csrf-check-disabled-globally": { + "canonical_rule_id": "l9:semgrep:python.pyramid.security.csrf-check-disabled-globally.pyramid-csrf-check-disabled-globally" + }, + "python.pyramid.security.direct-use-of-response.pyramid-direct-use-of-response": { + "canonical_rule_id": "l9:semgrep:python.pyramid.security.direct-use-of-response.pyramid-direct-use-of-response" + }, + "python.pyramid.security.sqlalchemy-sql-injection.pyramid-sqlalchemy-sql-injection": { + "canonical_rule_id": "l9:semgrep:python.pyramid.security.sqlalchemy-sql-injection.pyramid-sqlalchemy-sql-injection" + }, + "python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text": { + "canonical_rule_id": "l9:semgrep:python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text" + }, + "python.sqlalchemy.security.sqlalchemy-sql-injection.sqlalchemy-sql-injection": { + "canonical_rule_id": "l9:semgrep:python.sqlalchemy.security.sqlalchemy-sql-injection.sqlalchemy-sql-injection" + }, + "python.twilio.security.twiml-injection.twiml-injection": { + "canonical_rule_id": "l9:semgrep:python.twilio.security.twiml-injection.twiml-injection" + } + }, + "note": "Generated 2026-07-23 from the exact registry configs each preset invokes (semgrep show dump-config / raw registry fetch). Covers every rule Semgrep can emit today for this pack. New upstream registry rules require regenerating this file \u2014 see docs/identity-map-policy.md in l9-ci-core." +} diff --git a/.github/governance/semgrep-policy.yaml b/.github/governance/semgrep-policy.yaml new file mode 100644 index 0000000..eeda8e3 --- /dev/null +++ b/.github/governance/semgrep-policy.yaml @@ -0,0 +1,11 @@ +{ + "schema": "l9.finding-policy/v1", + "metadata": { + "version": "1" + }, + "defaults": { + "mode": "advisory" + }, + "rules": {}, + "note": "All findings default to advisory (visible, non-blocking) until a specific provider_rule_id is promoted to blocking here after org review. This is deliberate: identity-map + strict mode being wired should not silently convert every existing semgrep finding into a merge-blocker on first activation." +} diff --git a/.github/governance/waivers.yaml b/.github/governance/waivers.yaml new file mode 100644 index 0000000..dce98fa --- /dev/null +++ b/.github/governance/waivers.yaml @@ -0,0 +1,4 @@ +{ + "schema": "l9.waivers/v1", + "waivers": [] +} diff --git a/.github/workflows/l9-analysis.yml b/.github/workflows/l9-analysis.yml new file mode 100644 index 0000000..aed27bf --- /dev/null +++ b/.github/workflows/l9-analysis.yml @@ -0,0 +1,183 @@ +# L9 Governed Analysis Pipeline — Python Preset (LOCKED) +# +# DO NOT EDIT — this file is managed by l9-ci-core presets/python. +# To update, pull the latest preset from Quantum-L9/l9-ci-core. +# +# This workflow runs the full L9 analysis pipeline: +# 1. Resolve governance config from .github/governance/ +# 2. Run semgrep with Python rulesets +# 3. Provision the SDK (immutable, pinned) +# 4. Normalize → Validate → Project → Route → Manifest → Upload +# 5. Publish results as GitHub Checks +name: L9 Analysis +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +env: + L9_CORE_REF: "f88116503430aa18992b70d8d31063e34ff97ef1" + L9_PROFILE: "pr_fast" + L9_MATRIX_ID: "pr-semgrep" + +permissions: + contents: read + checks: write + +concurrency: + group: l9-analysis-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Governed Semgrep Analysis + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + enabled: ${{ steps.gov.outputs.enabled }} + mode: ${{ steps.gov.outputs.mode }} + governance-digest: ${{ steps.gov.outputs.governance-digest }} + artifact-name: ${{ steps.names.outputs.artifact-name }} + permissions: + contents: read + checks: write + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin \ + "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" + git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + git remote set-url origin "https://github.com/${REPOSITORY}.git" + + - id: gov + name: Resolve governance + uses: Quantum-L9/l9-ci-core/.github/actions/resolve-governance@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + profile: ${{ env.L9_PROFILE }} + provider: semgrep + event-name: ${{ github.event_name }} + repository: ${{ github.repository }} + ref: ${{ github.ref }} + + - id: names + name: Compute artifact names + env: + MATRIX_ID: ${{ env.L9_MATRIX_ID }} + run: | + set -euo pipefail + echo "artifact-name=l9-semgrep-${MATRIX_ID}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + + - name: Run semgrep + if: steps.gov.outputs.enabled == 'true' + run: | + set -euo pipefail + pip install --upgrade pip semgrep + mkdir -p "artifacts/raw/semgrep/${L9_MATRIX_ID}" + semgrep scan \ + --config p/python \ + --json \ + --output "artifacts/raw/semgrep/${L9_MATRIX_ID}/report.json" \ + --error --quiet || true + env: + L9_MATRIX_ID: ${{ env.L9_MATRIX_ID }} + + - id: sdk + name: Provision immutable SDK + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/provision-sdk@f88116503430aa18992b70d8d31063e34ff97ef1 + + - name: Normalize provider report + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/invoke-sdk@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + executable: ${{ steps.sdk.outputs.executable }} + operation: semgrep-normalize + input: artifacts/raw/semgrep/${{ env.L9_MATRIX_ID }}/report.json + output: .l9/runtime/${{ env.L9_MATRIX_ID }}/finding-bundle.json + root: . + snapshot-id: ${{ github.sha }} + revision: ${{ github.sha }} + strict: ${{ steps.gov.outputs.strict }} + required: ${{ steps.gov.outputs.required-provider }} + policy: ${{ steps.gov.outputs.sdk-policy }} + identity-map: .github/governance/semgrep-identity-map.yaml + + - name: Validate canonical bundle + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/validate-bundle@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + executable: ${{ steps.sdk.outputs.executable }} + bundle: .l9/runtime/${{ env.L9_MATRIX_ID }}/finding-bundle.json + + - name: Project agent-review payload + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/invoke-sdk@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + executable: ${{ steps.sdk.outputs.executable }} + operation: bundle-project-agent-payload + input: .l9/runtime/${{ env.L9_MATRIX_ID }}/finding-bundle.json + output: .l9/runtime/${{ env.L9_MATRIX_ID }}/agent-review-payload.json + strict: ${{ steps.gov.outputs.strict }} + + - id: route + name: Route artifacts + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/route-artifacts@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + provider: semgrep + matrix-id: ${{ env.L9_MATRIX_ID }} + raw-report: artifacts/raw/semgrep/${{ env.L9_MATRIX_ID }}/report.json + bundle: .l9/runtime/${{ env.L9_MATRIX_ID }}/finding-bundle.json + agent-payload: .l9/runtime/${{ env.L9_MATRIX_ID }}/agent-review-payload.json + destination-root: artifacts + + - name: Build artifact manifest + if: steps.gov.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/actions/build-artifact-manifest@f88116503430aa18992b70d8d31063e34ff97ef1 + with: + provider: semgrep + matrix-id: ${{ env.L9_MATRIX_ID }} + sdk-revision: ${{ steps.sdk.outputs.sdk-revision }} + bundle: ${{ steps.route.outputs.bundle }} + agent-payload: ${{ steps.route.outputs.agent-payload }} + raw-directory: ${{ steps.route.outputs.raw-directory }} + output: artifacts/metadata/${{ env.L9_MATRIX_ID }}/artifact-manifest.json + + - name: Upload analysis artifact set + if: steps.gov.outputs.enabled == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ steps.names.outputs.artifact-name }} + path: | + artifacts/raw/semgrep/${{ env.L9_MATRIX_ID }}/ + artifacts/l9/${{ env.L9_MATRIX_ID }}/ + artifacts/metadata/${{ env.L9_MATRIX_ID }}/ + if-no-files-found: error + retention-days: 14 + + publish: + name: Publish analysis (Core) + needs: analyze + if: needs.analyze.outputs.enabled == 'true' + uses: Quantum-L9/l9-ci-core/.github/workflows/publish-analysis.yml@f88116503430aa18992b70d8d31063e34ff97ef1 + permissions: + actions: read + checks: write + contents: read + with: + artifact-name: ${{ needs.analyze.outputs.artifact-name }} + profile: pr_fast + mode: ${{ needs.analyze.outputs.mode }} + provider: semgrep + matrix-id: pr-semgrep + governance-digest: ${{ needs.analyze.outputs.governance-digest }} + repository-revision: ${{ github.sha }} + workflow-result: ${{ needs.analyze.result }} diff --git a/.github/workflows/l9-lint-test.yml b/.github/workflows/l9-lint-test.yml new file mode 100644 index 0000000..f286208 --- /dev/null +++ b/.github/workflows/l9-lint-test.yml @@ -0,0 +1,142 @@ +# L9 Consumer Lint + Test — Python Preset (LOCKED STRUCTURE) +# +# DO NOT EDIT workflow structure — only the env: block is consumer-configurable. +# Managed by l9-ci-core presets/python. +# +# Conventions: +# - Immutable event-revision checkout (no floating action ref) +# - SHA-pinned external actions +# - Least privilege (contents: read only) +name: L9 Lint and Test +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +# ── CONFIGURABLE: agent sets these during activation ─────────────────────────── +env: + PYTHON_VERSION: "3.12" + SOURCE_DIR: "src/" + TEST_DIR: "tests/" + COVERAGE_THRESHOLD: "0" + PYTEST_PARALLEL: "false" + MYPY_EXCLUDE: "" + +permissions: + contents: read + +concurrency: + group: l9-lint-test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint and Type Check + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin \ + "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" + git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + git remote set-url origin "https://github.com/${REPOSITORY}.git" + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install lint tools + run: | + set -euo pipefail + python -m pip install --upgrade pip + if [ -f requirements-ci.txt ]; then pip install -r requirements-ci.txt; fi + command -v ruff >/dev/null 2>&1 || pip install ruff + command -v mypy >/dev/null 2>&1 || pip install mypy + + - name: ruff check + run: ruff check "${SOURCE_DIR}" --output-format=github + + - name: ruff format --check + run: ruff format --check "${SOURCE_DIR}" + + - name: mypy + run: | + set -euo pipefail + # mkdir required before --install-types on a cold cache, else mypy + # fails with the misleading "no mypy cache directory" error and + # masks any real type errors underneath it (python/mypy#10768). + mkdir -p .mypy_cache + MYPY_EXCLUDE_ARGS=() + if [ -n "${MYPY_EXCLUDE}" ]; then + MYPY_EXCLUDE_ARGS=(--exclude "${MYPY_EXCLUDE}") + fi + mypy "${SOURCE_DIR}" \ + "${MYPY_EXCLUDE_ARGS[@]}" \ + --show-error-codes --pretty \ + --install-types --non-interactive --ignore-missing-imports + + test: + name: Test Suite + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin \ + "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" + git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + git remote set-url origin "https://github.com/${REPOSITORY}.git" + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install test tools + run: | + set -euo pipefail + python -m pip install --upgrade pip + if [ -f requirements-ci.txt ]; then pip install -r requirements-ci.txt; fi + pip install -e ".[dev]" || pip install -e . || \ + echo "::notice::no local editable package to install" + command -v pytest >/dev/null 2>&1 || pip install pytest + python -c "import pytest_cov" 2>/dev/null || pip install pytest-cov + python -c "import xdist" 2>/dev/null || pip install pytest-xdist + python -c "import pytest_timeout" 2>/dev/null || pip install pytest-timeout + + - name: pytest with coverage + env: + TESTING: "true" + run: | + set -euo pipefail + XDIST_ARGS=() + if [ "${PYTEST_PARALLEL}" = "true" ]; then + XDIST_ARGS=(-n auto) + fi + PYTHONPATH=. pytest "${TEST_DIR}" \ + "${XDIST_ARGS[@]}" \ + --cov="${SOURCE_DIR}" \ + --cov-report=xml --cov-report=term-missing \ + --cov-fail-under="${COVERAGE_THRESHOLD}" \ + --timeout=300 --tb=short -v