From 530d3721927b5a658a393d841057f1ee49457ba7 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 23 Apr 2026 19:48:46 -0700 Subject: [PATCH 1/4] Addressed 2 security bugs - removed echo of credential value - Use DBMS_ASSERT to prevent SQL injection --- src/select_ai/credential.py | 4 +--- src/select_ai/privilege.py | 19 +++++++++++++++---- src/select_ai/sql.py | 14 ++++++++++++-- src/select_ai/version.py | 2 +- tests/conftest.py | 2 +- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/select_ai/credential.py b/src/select_ai/credential.py index d36f940..2946553 100644 --- a/src/select_ai/credential.py +++ b/src/select_ai/credential.py @@ -32,9 +32,7 @@ def _validate_credential(credential: Mapping[str, str]): } for k in credential.keys(): if k.lower() not in valid_keys: - raise ValueError( - f"Invalid value {k}: {credential[k]} for credential object" - ) + raise ValueError(f"Invalid key {k} for credential object") async def async_create_credential(credential: Mapping, replace: bool = False): diff --git a/src/select_ai/privilege.py b/src/select_ai/privilege.py index e2fcaef..1bf4a56 100644 --- a/src/select_ai/privilege.py +++ b/src/select_ai/privilege.py @@ -15,6 +15,13 @@ ) +def _normalize_schema_user(user: str) -> str: + user = user.strip() + if len(user) >= 2 and user[0] == '"' and user[-1] == '"': + return user + return user.upper() + + async def async_grant_privileges(users: Union[str, List[str]]): """ This method grants execute privilege on the packages DBMS_CLOUD, @@ -26,7 +33,8 @@ async def async_grant_privileges(users: Union[str, List[str]]): async with async_cursor() as cr: for user in users: - await cr.execute(GRANT_PRIVILEGES_TO_USER.format(user.strip())) + cr_user = _normalize_schema_user(user) + await cr.execute(GRANT_PRIVILEGES_TO_USER, user=cr_user) async def async_revoke_privileges(users: Union[str, List[str]]): @@ -40,7 +48,8 @@ async def async_revoke_privileges(users: Union[str, List[str]]): async with async_cursor() as cr: for user in users: - await cr.execute(REVOKE_PRIVILEGES_FROM_USER.format(user.strip())) + cr_user = _normalize_schema_user(user) + await cr.execute(REVOKE_PRIVILEGES_FROM_USER, user=cr_user) async def async_grant_http_access( @@ -90,7 +99,8 @@ def grant_privileges(users: Union[str, List[str]]): users = [users] with cursor() as cr: for user in users: - cr.execute(GRANT_PRIVILEGES_TO_USER.format(user.strip())) + cr_user = _normalize_schema_user(user) + cr.execute(GRANT_PRIVILEGES_TO_USER, user=cr_user) def revoke_privileges(users: Union[str, List[str]]): @@ -102,7 +112,8 @@ def revoke_privileges(users: Union[str, List[str]]): users = [users] with cursor() as cr: for user in users: - cr.execute(REVOKE_PRIVILEGES_FROM_USER.format(user.strip())) + cr_user = _normalize_schema_user(user) + cr.execute(REVOKE_PRIVILEGES_FROM_USER, user=cr_user) def grant_http_access(users: Union[str, List[str]], provider_endpoint: str): diff --git a/src/select_ai/sql.py b/src/select_ai/sql.py index 7aeab78..4eaaa39 100644 --- a/src/select_ai/sql.py +++ b/src/select_ai/sql.py @@ -9,7 +9,12 @@ DECLARE TYPE array_t IS VARRAY(4) OF VARCHAR2(60); v_packages array_t; + v_user VARCHAR2(261); BEGIN + v_user := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SCHEMA_NAME(:user), + FALSE + ); v_packages := array_t( 'DBMS_CLOUD', 'DBMS_CLOUD_AI', @@ -18,7 +23,7 @@ ); FOR i in 1..v_packages.count LOOP EXECUTE IMMEDIATE - 'GRANT EXECUTE ON ' || v_packages(i) || ' TO {0}'; + 'GRANT EXECUTE ON ' || v_packages(i) || ' TO ' || v_user; END LOOP; END; """ @@ -27,7 +32,12 @@ DECLARE TYPE array_t IS VARRAY(4) OF VARCHAR2(60); v_packages array_t; + v_user VARCHAR2(261); BEGIN + v_user := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SCHEMA_NAME(:user), + FALSE + ); v_packages := array_t( 'DBMS_CLOUD', 'DBMS_CLOUD_AI', @@ -36,7 +46,7 @@ ); FOR i in 1..v_packages.count LOOP EXECUTE IMMEDIATE - 'REVOKE EXECUTE ON ' || v_packages(i) || ' FROM {0}'; + 'REVOKE EXECUTE ON ' || v_packages(i) || ' FROM ' || v_user; END LOOP; END; """ diff --git a/src/select_ai/version.py b/src/select_ai/version.py index 499fd8c..472aff4 100644 --- a/src/select_ai/version.py +++ b/src/select_ai/version.py @@ -5,4 +5,4 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -__version__ = "1.3.0" +__version__ = "1.3.1" diff --git a/tests/conftest.py b/tests/conftest.py index dc8689c..a191ef5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,7 +71,7 @@ def _grant_basic_schema_privileges(cur, username: str): def _grant_select_ai_privileges(cur, username: str): try: - cur.execute(GRANT_PRIVILEGES_TO_USER.format(username.strip())) + cur.execute(GRANT_PRIVILEGES_TO_USER, user=username) except Exception as exc: msg = str(exc) if ( From b583488c179f232073ed949375fc7f2c918b0ce1 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 29 Apr 2026 18:12:35 -0700 Subject: [PATCH 2/4] For SynthenticDataParams, JSON-string params are not coerced into Class type --- src/select_ai/synthetic_data.py | 8 ++++++++ tests/gsd/test_2000_synthetic_data.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/select_ai/synthetic_data.py b/src/select_ai/synthetic_data.py index 34dcebe..a047af5 100644 --- a/src/select_ai/synthetic_data.py +++ b/src/select_ai/synthetic_data.py @@ -61,6 +61,14 @@ class SyntheticDataAttributes(SelectAIDataClass): user_prompt: Optional[str] = None def __post_init__(self): + super().__post_init__() + + if isinstance(self.params, str): + self.params = json.loads(self.params) + + if isinstance(self.params, Mapping): + self.params = SyntheticDataParams(**self.params) + if self.params and not isinstance(self.params, SyntheticDataParams): raise TypeError( "'params' must be an object of" " type SyntheticDataParams'" diff --git a/tests/gsd/test_2000_synthetic_data.py b/tests/gsd/test_2000_synthetic_data.py index 89b753e..4662dbd 100644 --- a/tests/gsd/test_2000_synthetic_data.py +++ b/tests/gsd/test_2000_synthetic_data.py @@ -189,3 +189,15 @@ def test_2008_generate_with_none_attributes(synthetic_profile): ValueError, match="'synthetic_data_attributes' cannot be None" ): synthetic_profile.generate_synthetic_data(None) + + +def test_2009_params_json_string_is_coerced(): + """JSON-string params are coerced into SyntheticDataParams""" + attributes = SyntheticDataAttributes( + object_name="people", + params='{"sample_rows": 1, "table_statistics": "true"}', + ) + + assert isinstance(attributes.params, SyntheticDataParams) + assert attributes.params.sample_rows == 1 + assert attributes.params.table_statistics is True From 8014ae04c13b9f1552a99519e4b280974598d158 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 13 May 2026 20:55:44 -0700 Subject: [PATCH 3/4] Added macaron Github Actions file to run as part of CI/CD pipeline --- .github/workflows/macaron.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/macaron.yaml diff --git a/.github/workflows/macaron.yaml b/.github/workflows/macaron.yaml new file mode 100644 index 0000000..86170d0 --- /dev/null +++ b/.github/workflows/macaron.yaml @@ -0,0 +1,25 @@ +name: macaron_security_analysis + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + + steps: + - name: Check out python-select-ai repository code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run Macaron GitHub Actions policy + uses: oracle/macaron@4ddb55e3c9ef2c77b548be55c557078c4476fd9c # v0.24.0 + with: + repo_path: ./ + policy_file: check-github-actions + policy_purl: "pkg:github.com/oracle/python-select-ai@.*" + reports_retention_days: 90 From 527fe9c722148e12aeeb56b8835ff5775ae52023 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 13 May 2026 20:59:13 -0700 Subject: [PATCH 4/4] Fixed policy violations --- .github/workflows/test.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 859dcdb..9ab95e6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,9 @@ name: select_ai_py_tests on: push +permissions: + contents: read + jobs: test: runs-on: ${{ matrix.os }} @@ -12,10 +15,10 @@ jobs: steps: - name: Check out python-select-ai repository code - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }}