Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: [main]
workflow_dispatch:

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

Expand All @@ -31,6 +34,7 @@ jobs:
mkdir -p sdk_generator/open_api_tool
wget -q https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.12.0/openapi-generator-cli-7.12.0.jar \
-O sdk_generator/open_api_tool/openapi-generator-cli-7.12.0.jar
echo "33e7dfa7a1f04d58405ee12ae19e2c6fc2a91497cf2e56fa68f1875a95cbf220 sdk_generator/open_api_tool/openapi-generator-cli-7.12.0.jar" | sha256sum -c

- name: Build SDK
run: |
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
release:
types: [published]

permissions:
contents: read

jobs:
build-n-publish:
name: Build and publish Python distributions to PyPI
Expand All @@ -17,12 +20,13 @@ jobs:
- uses: actions/setup-java@17f84c3641ba7b8f6deff6309fc4c864478f5d62 # v3
with:
distribution: 'zulu'
java-version: '8'
java-version: '11'
- name: Download OpenAPI Generator
run: |
mkdir -p sdk_generator/open_api_tool
wget -q https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.6.0/openapi-generator-cli-6.6.0.jar \
-O sdk_generator/open_api_tool/openapi-generator-cli-6.6.0
wget -q https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.12.0/openapi-generator-cli-7.12.0.jar \
-O sdk_generator/open_api_tool/openapi-generator-cli-7.12.0.jar
echo "33e7dfa7a1f04d58405ee12ae19e2c6fc2a91497cf2e56fa68f1875a95cbf220 sdk_generator/open_api_tool/openapi-generator-cli-7.12.0.jar" | sha256sum -c
- name: Build SDK
run: |
python -m pip install -r ./requirements.txt
Expand Down
28 changes: 17 additions & 11 deletions chkp_ai_security_sdk/core/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
from chkp_ai_security_sdk.core.logger import logger, error_logger
from chkp_ai_security_sdk.core.sdk_platform import KEEP_ALIVE_GRACE_SECONDS

_token_lock = threading.Lock()

CI_AUTH_PATH = '/auth/external'
SOURCE_HEADER = 'ai-security-py-sdk'
VERIFY_CONTENT = False


class SessionManager:
Expand All @@ -31,9 +32,11 @@ def __init__(self):
def client_configuration(self) -> Any:
self.__check_connected()
from chkp_ai_security_sdk.generated.configuration import Configuration
with _token_lock:
token = self.__jwt_token
configuration = Configuration()
configuration.host = self.__url
configuration.access_token = self.__jwt_token
configuration.access_token = token
configuration.client_id = self.__infinity_portal_auth.client_id if self.__infinity_portal_auth else None
return configuration

Expand All @@ -46,25 +49,27 @@ def __perform_ci_login(self):
'accessKey': self.__infinity_portal_auth.access_key,
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url=auth_url, data=json.dumps(payload), headers=headers)
response = requests.post(url=auth_url, data=json.dumps(payload), headers=headers, timeout=30)

if not 200 <= response.status_code <= 299:
error_logger(f'CI login failed with status "{response.status_code}" payload: "{response.text}"')
error_logger(f'CI login failed with status "{response.status_code}" for session "{self.__session_id}"')
truncated_body = (response.text or '')[:500]
raise WorkforceAIApiException(
error_scope=WorkforceAIErrorScope.SERVICE,
payload_error=response.text,
payload_error=truncated_body,
url=auth_url,
status_code=response.status_code,
)

response_json = response.json()
if not response_json.get('success'):
error_logger(f'CI login failed for session "{self.__session_id}", error: {response_json}')
raise WorkforceAIApiException(error_scope=WorkforceAIErrorScope.SERVICE, payload_error=str(response_json))
error_logger(f'CI login failed for session "{self.__session_id}"')
raise WorkforceAIApiException(error_scope=WorkforceAIErrorScope.SERVICE, payload_error=str(response_json)[:500])

self.__jwt_token = response_json['data']['token']
self.__token_expires_in = response_json['data'].get('expiresIn', 1800)
self.__sdk_connection_state = SDKConnectionState.CONNECTED
with _token_lock:
self.__jwt_token = response_json['data']['token']
self.__token_expires_in = response_json['data'].get('expiresIn', 1800)
self.__sdk_connection_state = SDKConnectionState.CONNECTED
logger(f'CI login succeeded for session "{self.__session_id}", token expires in {self.__token_expires_in}s')

except WorkforceAIApiException:
Expand Down Expand Up @@ -139,7 +144,8 @@ def disconnect(self):
self.__sdk_connection_state = SDKConnectionState.DISCONNECTED
self.__keep_alive_on_flag = False
self.__keep_alive_event.set() # Wake up sleeping thread so it exits
self.__jwt_token = ''
with _token_lock:
self.__jwt_token = ''
self.__session_id = str(uuid.uuid4())

def connection_state(self) -> SDKConnectionState:
Expand Down
1 change: 0 additions & 1 deletion examples/async_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ async def main():
result = await ai.chats_policy_api.get_chats_rulebase_external_v1_chats_rulebase_get()
print(result)


# Run multiple AI Security calls concurrently
print('\n--- Concurrent AI Security calls ---')
access, dlp = await asyncio.gather(
Expand Down
6 changes: 4 additions & 2 deletions resources/templates/python/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ from {{packageName}}.api_response import ApiResponse
from {{packageName}}.rest import RESTResponseType
# TEMPLATE EDIT: import SDK logger
from chkp_ai_security_sdk.core.logger import logger, error_logger, network_logger
import hashlib


{{#operations}}
Expand All @@ -38,7 +39,7 @@ class {{classname}}:
# TEMPLATE EDIT: log outgoing request
_client_id = getattr(self.api_client.configuration, 'client_id', None) or 'N/A'
logger(f'Sending operation "{{operationId}}"...')
network_logger(f'Sending "{{operationId}}" {{httpMethod}} "{{{path}}}" | clientId={_client_id[:8]}...')
network_logger(f'Sending "{{operationId}}" {{httpMethod}} "{{{path}}}"')
try:
response_data = {{#asyncio}}await {{/asyncio}}self.api_client.call_api(
*_param,
Expand Down Expand Up @@ -70,8 +71,9 @@ class {{classname}}:
{{>partial_api}}
# TEMPLATE EDIT: log outgoing request
_client_id = getattr(self.api_client.configuration, 'client_id', None) or 'N/A'
_client_id_hash = hashlib.sha256(_client_id.encode()).hexdigest()[:8] if _client_id != 'N/A' else 'N/A'
logger(f'Sending operation "{{operationId}}_with_http_info"...')
network_logger(f'Sending "{{operationId}}" {{httpMethod}} "{{{path}}}" | clientId={_client_id[:8]}...')
network_logger(f'Sending "{{operationId}}" {{httpMethod}} "{{{path}}}" | clientId=sha256:{_client_id_hash}')
try:
response_data = {{#asyncio}}await {{/asyncio}}self.api_client.call_api(
*_param,
Expand Down
9 changes: 7 additions & 2 deletions resources/templates/python/api_client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import decimal
import json
import mimetypes
import os
import pathlib
import re
import tempfile

Expand Down Expand Up @@ -718,8 +719,12 @@ class ApiClient:
r'filename=[\'"]?([^\'"\s]+)[\'"]?',
content_disposition
)
assert m is not None, "Unexpected 'content-disposition' header value"
filename = m.group(1)
if m is None:
raise ApiValueError("Unexpected 'content-disposition' header value")
filename = pathlib.PurePosixPath(m.group(1)).name
filename = pathlib.PureWindowsPath(filename).name
if not filename or filename in ('.', '..'):
raise ApiValueError("Invalid filename in 'content-disposition' header")
path = os.path.join(os.path.dirname(path), filename)

with open(path, "wb") as f:
Expand Down
28 changes: 15 additions & 13 deletions sdk_generator/generate_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def generate(product: dict):
project_dir = Path(this_dir_path, '../')
specs_path = os.path.join(project_dir, 'resources', 'specs', product['spec_dir'], 'swagger.json')
generator_path = os.path.join(this_dir_path, 'open_api_tool', 'openapi-generator-cli-7.12.0.jar')
jre_path = os.getenv('JRE_PATH', 'java')
jre_path = shutil.which('java')
if not jre_path:
raise RuntimeError('Java runtime not found. Install Java or ensure java is on PATH.')
generated_path = os.path.join(project_dir, product['generated_rel'])

library = product.get('library', '')
Expand All @@ -61,18 +63,18 @@ def generate(product: dict):
onerror=lambda err: print(f'{log_prefix} Error cleaning generated dir: {err}'),
)

cmd_line = (
f'"{jre_path}" -jar {generator_path} generate'
f' --generator-name python'
f' --input-spec {specs_path}'
f' --output {project_dir}'
f' --template-dir {template_dir}'
f' --global-property modelDocs=false,modelTests=false'
f' --additional-properties=generateSourceCodeOnly=true,packageName={product["package_name"]}{library_prop}'
f' --skip-validate-spec'
)
print(f'{log_prefix} Invoking generator:\n{cmd_line}')
subprocess.run(cmd_line, shell=True, check=True, stdout=sys.stdout)
cmd = [
jre_path, '-jar', str(generator_path), 'generate',
'--generator-name', 'python',
'--input-spec', str(specs_path),
'--output', str(project_dir),
'--template-dir', str(template_dir),
'--global-property', 'modelDocs=false,modelTests=false',
'--additional-properties', f'generateSourceCodeOnly=true,packageName={product["package_name"]}{library_prop}',
'--skip-validate-spec',
]
print(f'{log_prefix} Invoking generator:\n{" ".join(cmd)}')
subprocess.run(cmd, check=True, stdout=sys.stdout)

except subprocess.CalledProcessError as e:
print(f'{log_prefix} Generator error:\n\t{e}')
Expand Down
37 changes: 33 additions & 4 deletions sdk_generator/scripts/fetch_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
import re
import json
import shutil
import requests
from urllib.parse import urlparse

API_SPEC_OWNER = 'Check-Point'
SWAGGERHUB_API_KEY = os.environ.get('SWAGGERHUB_API_KEY')
Expand All @@ -16,15 +18,36 @@
OUTPUT_BASE_PATH = 'resources/specs'
SWAGGER_CONF = 'swagger.json'

ALLOWED_SPEC_PATTERN = re.compile(r'^[a-zA-Z0-9_-]+$')


def __validate_spec_name(name):
if not ALLOWED_SPEC_PATTERN.match(name):
raise ValueError(f'Invalid spec name "{name}": must be alphanumeric, hyphens, or underscores only')
return name


def __validate_local_path(local_path):
real = os.path.realpath(local_path)
project_root = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
if not real.startswith(project_root + os.sep):
raise ValueError(f'Local spec path must be within the project directory: {local_path}')
if not os.path.isfile(real):
raise ValueError(f'Local spec path does not exist or is not a file: {local_path}')
if not real.endswith('.json'):
raise ValueError(f'Local spec path must be a .json file: {local_path}')
return real


# Specs to fetch: (env var for local override, spec name, output subdir)
SPECS = [
{
'name': os.environ.get('SPEC_NAME', 'checkpoint-ai-security'),
'name': __validate_spec_name(os.environ.get('SPEC_NAME', 'checkpoint-ai-security')),
'local_path_env': 'LOCAL_GENERATED_API_PATH',
'output_dir': 'main',
},
{
'name': os.environ.get('BROWSE_SPEC_NAME', 'checkpoint-browse-security'),
'name': __validate_spec_name(os.environ.get('BROWSE_SPEC_NAME', 'checkpoint-browse-security')),
'local_path_env': 'LOCAL_BROWSE_SPEC_PATH',
'output_dir': 'browse',
},
Expand All @@ -43,12 +66,17 @@ def __deposit_file(path, filename, content):

def __download_spec(spec_name):
print(f'[fetch-api] Fetching spec "{spec_name}" from SwaggerHub...')
res = requests.get(f'https://api.swaggerhub.com/apis/{API_SPEC_OWNER}/{spec_name}', headers=swagger_headers)
res = requests.get(f'https://api.swaggerhub.com/apis/{API_SPEC_OWNER}/{spec_name}', headers=swagger_headers, timeout=30)
all_specs = res.json()
latest = all_specs['apis'][-1]
url = next((p['url'] for p in latest['properties'] if p['type'] == 'Swagger'), None)
if not url:
raise ValueError(f'No Swagger URL found for spec "{spec_name}"')
parsed = urlparse(url)
if parsed.scheme != 'https' or parsed.hostname != 'api.swaggerhub.com':
raise ValueError(f'Unexpected spec URL origin: {url}')
print(f'[fetch-api] Downloading from: {url}')
spec_res = requests.get(url, headers=swagger_headers)
spec_res = requests.get(url, headers=swagger_headers, timeout=30)
return spec_res.json()


Expand All @@ -64,6 +92,7 @@ def fetch_api_specs():

print(f'[fetch-api] Processing spec: {spec_name}')
if local_path:
local_path = __validate_local_path(local_path)
print(f'[fetch-api] Using local spec from: {local_path}')
shutil.copy(local_path, os.path.join(out_dir, SWAGGER_CONF))
else:
Expand Down
57 changes: 43 additions & 14 deletions sdk_generator/scripts/post_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,55 @@ def __prepare_build_info(product):
with open(os.path.join(spec_path, 'spec'), 'r') as f:
spec_name = f.readline().strip()

sdk_build = os.environ.get('BUILD_JOB_ID', '')
sdk_version = os.environ.get('BUILD_VERSION', '')
spec_version = swagger_spec['info']['version']
released_on = datetime.now().isoformat()

content = f'''
from {PKG_NAME}.classes.workforceai_sdk_info import WorkforceAISDKInfo
build_data = {
'sdk_build': os.environ.get('BUILD_JOB_ID', ''),
'sdk_version': os.environ.get('BUILD_VERSION', ''),
'spec': spec_name,
'spec_version': swagger_spec['info']['version'],
'released_on': datetime.now().isoformat(),
}

with open(os.path.join(output_path, 'sdk_build_data.json'), 'w') as f:
json.dump(build_data, f)

pkg_name = PKG_NAME
content = f'''import json
import os
from {pkg_name}.classes.workforceai_sdk_info import WorkforceAISDKInfo

def sdk_build_info() -> WorkforceAISDKInfo:
return WorkforceAISDKInfo(
sdk_build="{sdk_build}",
sdk_version="{sdk_version}",
spec="{spec_name}",
spec_version="{spec_version}",
released_on="{released_on}",
)
data_path = os.path.join(os.path.dirname(__file__), 'sdk_build_data.json')
with open(data_path, 'r') as f:
data = json.load(f)
return WorkforceAISDKInfo(**data)
'''
with open(os.path.join(output_path, 'sdk_build.py'), 'w') as f:
f.write(content)
print(f'[post-build:{product["label"]}] sdk_build.py written')


def __patch_configuration(product):
"""Remove httplib.HTTPConnection.debuglevel changes from generated
configuration.py so that enabling SDK debug mode does not dump raw
HTTP traffic (including Authorization headers) to stdout."""
config_path = os.path.join(product['generated_dir'], 'configuration.py')
if not os.path.isfile(config_path):
return
with open(config_path, 'r') as f:
content = f.read()
original = content
# Remove the lines that toggle httplib debug level
content = content.replace(' httplib.HTTPConnection.debuglevel = 1\n', '')
content = content.replace(' httplib.HTTPConnection.debuglevel = 0\n', '')
# Remove the now-unused httplib import if no other references remain
if 'httplib' not in content.split('import http.client as httplib')[-1]:
content = content.replace('import http.client as httplib\n', '')
if content != original:
with open(config_path, 'w') as f:
f.write(content)
print(f'[post-build:{product["label"]}] Patched configuration.py — removed httplib debug exposure')


def __cleanup_generated(product):
output_path = product['generated_dir']
for dirname in ['test', 'docs']:
Expand Down Expand Up @@ -100,6 +127,8 @@ def post_build_process():
label = product['label']
print(f'[post-build:{label}] Preparing build info...')
__prepare_build_info(product)
print(f'[post-build:{label}] Patching generated code...')
__patch_configuration(product)
print(f'[post-build:{label}] Cleaning up generated files...')
__cleanup_generated(product)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
maintainer_email='haimk@checkpoint.com',
url='https://github.com/CheckPointSW/ai-security-py-sdk',
packages=find_packages(exclude=['sdk_generator', 'scripts', 'tests']),
package_data={'': ['*']},
package_data={'': ['*.json']},
install_requires=prod_dependencies,
python_requires='>=3.9,<4.0',
)
Loading