diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeced60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +*__pycache__* +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# IDE + +.idea/ + + +# docker/helm +docker/certificates/**/* +rules.py + +# demo +demo.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f8c9d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Donghui Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..405be7a --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Python wrapper for JFROG Xray REST API +`jfrog-xray-api` is a live python package for JFrog Xray REST API. + +# Install +``` +pip install jfrog-xray-api +``` +# Usage + +## Authentication +```python +# User and password OR API_KEY +from xray import XrayRestClient +xray_rest_client = XrayRestClient( + base_url="http://localhost:8082/xray", + username='USERNAME', + password='PASSWORD or API_KEY' +) +``` +## Components +### Find Component by Name +```python +components = xray_rest_client.components +response = components.find_component_by_name("jenkinsapi") +print(response.json()) +``` +### Find Components by CVEs +```python +components = xray_rest_client.components +cve_list = ['CVE-2021-4104'] +response = components.find_components_by_cves(cve_list) +print(response.json()) +``` +### Find CVEs by Components +```python +components = xray_rest_client.components +components_id_list = ['gav://commons-collections:commons-collections:3.2.1', 'gav://commons-collections:commons-collections:3.2.2'] +response = components.find_cves_by_components(components_id_list) +print(response.json()) +``` +### Get Component List Per Watch +```python +# TODO +``` +### Get Artifact Dependency Graph +```python +components = xray_rest_client.components +artifact_path = '/Artifactory/pnnl/goss/goss-core-client/0.1.7/goss-core-client-0.1.7-sources.jar' +response = components.get_artifact_dependency_graph(artifact_path) +print(response.json()) +``` +### Compare Artifacts +```python +components = xray_rest_client.components +source_artifact_path = '/Artifactory/pnnl/goss/goss-core-client/0.1.7/goss-core-client-0.1.7-sources.jar' +target_artifact_path = '/Artifactory/pnnl/goss/goss-core-client/0.1.8/goss-core-client-0.1.8-sources.jar' +response = components.compare_artifacts(source_artifact_path, target_artifact_path) +print(response.json()) +``` +### Get Build Dependency Graph +```python +components = xray_rest_client.components +artifactory_instance = "myInstance", +build_name = "someBuild", +build_number = "someNumber" +response = components.get_build_dependency_graph(artifactory_instance, build_name, build_number) +print(response.json()) +``` +### Compare Builds +```python +components = xray_rest_client.components +response = components.compare_builds( + "my-instance", "someOriginBuild", "111", + "my-instance", "someTargetBuild", "222", +) +print(response.json()) +``` +### Export Component Details +```python +# TODO +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..87e49b1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "jfrog-xray-api" +version = "0.0.1" +authors = [ + { name="Donghui Wang", email="977675308@qq.com" }, +] +description = "Python wrapper for JFROG Xray REST API" +readme = "README.md" +requires-python = ">=3.7" +license = {file = "LICENSE.txt"} +keywords = [ + "jfrog", + "xray", + "jfrog-xray", + "devsecops" +] +dependencies = [ + "requests" +] +# see https://pypi.org/pypi?%3Aaction=list_classifiers +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + 'Topic :: Software Development' +] + +[project.urls] +homepage = "https://github.com/donhui/jfrog-xray-api" +repository = "https://github.com/donhui/jfrog-xray-api.git" +"Bug Tracker" = "https://github.com/donhui/jfrog-xray-api/issues" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3288e92 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_xray.py b/tests/test_xray.py new file mode 100644 index 0000000..58e5dfe --- /dev/null +++ b/tests/test_xray.py @@ -0,0 +1,10 @@ +import unittest + + +class MyTestCase(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == '__main__': + unittest.main() diff --git a/xray/__init__.py b/xray/__init__.py new file mode 100644 index 0000000..b90c472 --- /dev/null +++ b/xray/__init__.py @@ -0,0 +1,20 @@ +import requests +from requests.auth import HTTPBasicAuth + +from xray.system import XraySystem +from xray.components import XrayComponents + + +class XrayRestClient(object): + def __init__(self, *, base_url, username, password): + self.base_url = base_url + self._session = requests.Session() + self._session.auth = HTTPBasicAuth(username, password) + + @property + def system(self): + return XraySystem(base_url=self.base_url, session=self._session) + + @property + def components(self): + return XrayComponents(base_url=self.base_url, session=self._session) diff --git a/xray/components.py b/xray/components.py new file mode 100644 index 0000000..4ee9486 --- /dev/null +++ b/xray/components.py @@ -0,0 +1,159 @@ +from xray.utils.http import RestApiAccessor + + +class XrayComponents(RestApiAccessor): + """ + Xray REST API: COMPONENTS + See: https://www.jfrog.com/confluence/display/JFROG/Xray+REST+API#XrayRESTAPI-COMPONENTS + """ + + def find_component_by_name(self, name: str): + """ + Search for a component by name - applicable only for components synced from the JFrog Global database to Xray + :param name: + :return: + """ + assert len(str(name)) > 0 + url = self.base_url + "/api/v1/component/" + name + response = self.rest_get( + url + ) + return response + + def find_components_by_cves(self, cve_list: list): + """ + Search for components by the CVEs it contains directly + :param cve_list: + :return: + """ + assert len(cve_list) > 0 + url = self.base_url + "/api/v1/component/searchByCves" + json_data = { + "cves": cve_list + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def find_cves_by_components(self, components_id_list: list): + """ + Search for CVEs by the infected components + :param components_id_list: + :return: + """ + assert len(components_id_list) > 0 + url = self.base_url + "/api/v1/component/searchCvesByComponents" + json_data = { + "components_id": components_id_list + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def get_component_list_per_watch(self): + """ + Gets a list of components associated with a specific watch + :return: + """ + pass + + def get_artifact_dependency_graph(self, artifact_path: str): + """ + Get the complete dependency graph for an artifact + :param artifact_path: + :return: + """ + url = self.base_url + "/api/v1/dependencyGraph/artifact" + json_data = { + "path": artifact_path + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def compare_artifacts(self, source_artifact_path: str, target_artifact_path: str): + """ + Compares two artifacts and produces the difference between them + :param source_artifact_path: + :param target_artifact_path: + :return: + """ + url = self.base_url + "/api/v1/dependencyGraph/artifactDelta" + json_data = { + "source_artifact_path": source_artifact_path, + "target_artifact_path": target_artifact_path + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def get_build_dependency_graph(self, artifactory_id: str, build_name: str, build_number: str): + """ + Get the complete dependency graph for a build + :param artifactory_id: + :param build_name: + :param build_number: + :return: + """ + url = self.base_url + "/api/v1/dependencyGraph/build" + json_data = { + "artifactory_id": artifactory_id, + "build_name": build_name, + "build_number": build_number + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def compare_builds(self, + source_artifactory_id: str, + source_build_name: str, + source_build_number: str, + target_artifactory_id: str, + target_build_name: str, + target_build_number: str + ): + """ + Compares two builds and produces the difference between them + :param source_artifactory_id: + :param source_build_name: + :param source_build_number: + :param target_artifactory_id: + :param target_build_name: + :param target_build_number: + :return: + """ + url = self.base_url + "/api/v1/dependencyGraph/buildDelta" + json_data = { + "source_artifactory_id": source_artifactory_id, + "source_build_name": source_build_name, + "source_build_number": source_build_number, + "target_artifactory_id": target_artifactory_id, + "target_build_name": target_build_name, + "target_build_number": target_build_number + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def export_component_details(self): + """ + Export component details + TODO + :return: + """ + pass + + diff --git a/xray/general_settings.py b/xray/general_settings.py new file mode 100644 index 0000000..282f1a7 --- /dev/null +++ b/xray/general_settings.py @@ -0,0 +1,5 @@ +from xray.utils.http import RestApiAccessor + + +class XrayGeneralSettings(RestApiAccessor): + pass diff --git a/xray/system.py b/xray/system.py new file mode 100644 index 0000000..33e2213 --- /dev/null +++ b/xray/system.py @@ -0,0 +1,81 @@ +from xray.utils.http import RestApiAccessor + + +class XraySystem(RestApiAccessor): + """ + Xray REST API: SYSTEM + See: https://www.jfrog.com/confluence/display/JFROG/Xray+REST+API#XrayRESTAPI-SYSTEM + """ + + def get_version(self): + """ + Gets the Xray version and revision you are running + :return: + """ + url = self.base_url + "/api/v1/system/version" + response = self.rest_get( + url + ) + return response + + def get_metrics(self): + """ + Get system metrics data + :return: + """ + url = self.base_url + "/api/v1/metrics" + response = self.rest_get( + url + ) + return response + + def create_bundle(self, + bundle_name: str, + bundle_description='', + include_configuration=True, + include_system=True, + include_logs=False, + logs_start_date='2023-02-17T16:32:04+03:00', + logs_end_date='2023-02-17T16:32:04+03:00', + thread_dump_count=1, + thread_dump_interval=0, + ): + """ + Create support bundle + :return: + """ + url = self.base_url + "/api/v1/system/support/bundle" + json_data = { + "name": bundle_name, + "description": bundle_description, + "parameters": { + "configuration": include_configuration, + "system": include_system, + "logs": { + "end_date": logs_end_date, + "include": include_logs, + "start_date": logs_start_date, + }, + "thread_dump": { + "count": thread_dump_count, + "interval": thread_dump_interval + } + } + } + response = self.rest_post( + url, + json_data=json_data + ) + return response + + def send_ping(self): + """ + Sends a ping request + :return: + """ + url = self.base_url + "/api/v1/system/ping" + response = self.rest_get( + url + ) + return response + diff --git a/xray/utils/__init__.py b/xray/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xray/utils/env.py b/xray/utils/env.py new file mode 100644 index 0000000..32e305a --- /dev/null +++ b/xray/utils/env.py @@ -0,0 +1,8 @@ +import os + + +def get_env(key): + v = os.getenv(key=key) + if v is None: + raise Exception('The environment %s does not exists.' % key) + return v diff --git a/xray/utils/http.py b/xray/utils/http.py new file mode 100644 index 0000000..38f30f2 --- /dev/null +++ b/xray/utils/http.py @@ -0,0 +1,61 @@ +class RestApiAccessor: + + def __init__(self, base_url, session): + self._session = session + self.base_url = base_url + + """ + Implements operations with REST API + """ + + def rest_get(self, + url, + params=None, + headers=None, + verify=True, + cert=None, + timeout=None, + ): + """ + Perform a GET request to url + :param url: + :param params: + :param headers: + :param verify: + :param cert: + :param timeout: + :return: response object + """ + response = self._session.get( + url, + params=params, + headers=headers, + verify=verify, + cert=cert, + timeout=timeout, + ) + return response + + def rest_post(self, + url, + params=None, + headers=None, + verify=True, + cert=None, + timeout=None, + json_data=None, + ): + """ + Perform a POST request to url + """ + response = self._session.post( + url, + json=json_data, + params=params, + headers=headers, + verify=verify, + cert=cert, + timeout=timeout, + ) + response.raise_for_status() + return response