diff --git a/sdk/.gitignore b/sdk/.gitignore new file mode 100644 index 000000000..ce09c36c2 --- /dev/null +++ b/sdk/.gitignore @@ -0,0 +1,4 @@ +venv +__pycache__ +.idea +.DS-store \ No newline at end of file diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 000000000..879f23eab --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,89 @@ +# OpenCRE SDK + +The OpenCRE SDK is a Python (3.11 and later) library that provides a convenient interface for interacting with the OpenCRE API. It includes classes for handling Common Requirements Enumeration (CRE) data, links, and associated documents. + + +## Installation + +```bash +pip install opencre-sdk +``` + +## Usage + + +### Create an OpenCRE instance + +```python +from opencre import OpenCRE + +# Initialize OpenCRE SDK +opencre = OpenCRE() +``` + +### Configuration +The `OpenCREConfig` class allows you to configure the OpenCRE SDK with your specific settings. By default, it is configured with the OpenCRE base URL and API prefix. + +```python +from opencre_sdk import OpenCREConfig, OpenCRE + +# Create an OpenCRE configuration instance +config = OpenCREConfig() + +# Optionally, customize the configuration +config.HOST_URL = "https://example.org/" +config.API_PREFIX = "custom/v1/" + +# Create an OpenCRE instance with the custom configuration +opencre = OpenCRE(config=config) +``` + +## Interacting with CREs + +The `OpenCRE` class provides methods for interacting with CREs: + +### Retrieving Root CREs + +To retrieve a list of root CREs: + +```python +root_cres = opencre.root_cres() +print(root_cres) +``` + +### Retrieve a specific CRE by ID + +```python +cre_id = "170-772" +cre = opencre.cre(cre_id) +print(str(cre)) # Outputs: 'CRE 170-772' +print(cre.name) # Outputs: 'Cryptography' +print(cre.id) # Outputs: '170-772' +``` + +## Handling Links and Documents + +The `Link` class represents a link associated with a CRE, and the `Document` class represents a document associated with a CRE. Additional document types (`Standard`, `Tool`, and `CRELink`) extend the `Document` class. + +### Access links of a CRE + +```python +cre = opencre.cre("170-772") +links = cre.links +link = links[5] +print(link.ltype) # Outputs: 'Linked To' +doc = link.document +print(doc.name) # Outputs: 'Cloud Controls Matrix' +``` + +#### Link Types (ltype) + +`ltype` attribute in the `Link` class represents the type of relationship between the CRE and the linked document. Currently, there are two possible values for ltype: + +- **`Contains`**: This indicates that the CRE ncludes the content of the linked document. For instance, a CRE about "Manual penetration testing" might contain another CRE about "Dynamic security testing". + +- **`Linked To`**: This signifies a reference to an external standard, tool, or another CRE. For example, a CRE might be linked to a specific section in the "NIST SSDF" standard. + +## Contributing + +We welcome contributions! Please submit pull requests for bug fixes, features, and improvements. See [**Contributing**](https://github.com/OWASP/OpenCRE/blob/main/CONTRIBUTING.md) for contributing instructions. diff --git a/sdk/opencre/__init__.py b/sdk/opencre/__init__.py new file mode 100644 index 000000000..c4f97a077 --- /dev/null +++ b/sdk/opencre/__init__.py @@ -0,0 +1 @@ +from .opencre import OpenCRE diff --git a/sdk/opencre/models.py b/sdk/opencre/models.py new file mode 100644 index 000000000..0926ec504 --- /dev/null +++ b/sdk/opencre/models.py @@ -0,0 +1,196 @@ +from __future__ import annotations +from requests import Response + + +class Link: + """ + Represents a link associated with a CRE. + + Attributes: + - raw (dict): The raw data of the link. + - ltype (str): The type of the link. + + Methods: + - parse_from_cre(cls, cre: CRE) -> list[Link]: Parses links from a CRE object. + - get_document_class(self): Determines the document class associated with the link's doctype. + - document(self): Retrieves the associated document instance. + """ + def __init__(self, raw, ltype): + self.raw = raw + self.ltype = ltype + + @classmethod + def parse_from_cre(cls, cre: CRE) -> list[Link]: + """ + Parses links from a CRE object. + + Parameters: + - cre (CRE): The CRE object. + + Returns: + list[Link]: A list of Link objects parsed from the CRE. + """ + links_raw = cre.raw.get("links") + links = [cls(raw=link_raw, ltype=link_raw["ltype"]) for link_raw in links_raw] + return links + + def get_document_class(self): + """ + Determines the document class associated with the link's doctype. + + Returns: + document_class: The class of the associated document. + """ + document_parent = Document.parse_from_link(link=self) + doctype = document_parent.doctype + document_class = None + + if doctype == "Standard": + document_class = Standard + + if doctype == "CRE": + document_class = CRELink + + if doctype == "Tool": + document_class = Tool + + if document_class is None: + raise NotImplementedError("Not implemented for this doctype") + + return document_class + + @property + def document(self): + """ + Retrieves the associated document instance. + + Returns: + Document: The associated document instance. + """ + document_class = self.get_document_class() + document = document_class.parse_from_link(self) + return document + + +class CRE: + """ + Represents a CRE (Common Requirements Enumeration). + + Attributes: + - raw (dict): The raw data of the CRE. + - id (str): The identifier of the CRE. + - name (str): The name of the CRE. + - doctype (str): The type of the CRE. + + Methods: + - links(self): Retrieves links associated with the CRE. + - parse_from_response(cls, response: Response, many: bool = False) -> CRE | list[CRE]: Parses CRE(s) from a response object. + - __str__(self): Returns a string representation of the CRE. + """ + def __init__(self, raw, cre_id, name, doctype): + self.raw = raw + self.id = cre_id + self.name = name + self.doctype = doctype + + @property + def links(self): + """ + Retrieves links associated with the CRE. + + Returns: + list[Link]: A list of Link objects associated with the CRE. + """ + return Link.parse_from_cre(cre=self) + + @classmethod + def parse_from_response(cls, response: Response, many: bool = False) -> CRE | list[CRE]: + """ + Parses CRE(s) from a response object. + + Parameters: + - response (Response): The response object. + - many (bool): True if expecting multiple CREs in the response, False otherwise. + + Returns: + CRE | list[CRE]: A single CRE or a list of CREs parsed from the response. + """ + cres = [] + data = response.json().get("data") + + if not many: + data = [data] + + for raw_cre in data: + cre = cls( + raw=raw_cre, + cre_id=raw_cre["id"], + name=raw_cre["name"], + doctype=raw_cre["doctype"] + ) + cres.append(cre) + + if not many: + return cres[0] + + return cres + + def __str__(self): + return f'CRE {self.id}' + + +class Document: + """ + Represents a document associated with a CRE. + + Attributes: + - raw (dict): The raw data of the document. + - doctype (str): The type of the document. + - name (str): The name of the document. + + Methods: + - parse_from_link(cls, link: Link) -> Document: Parses a document from a Link object. + """ + def __init__(self, raw, doctype, name): + self.raw = raw + self.doctype = doctype + self.name = name + + @classmethod + def parse_from_link(cls, link: Link) -> Document: + """ + Parses a document from a Link object. + + Parameters: + - link (Link): The Link object. + + Returns: + Document: The Document object associated with the Link. + """ + document_raw = link.raw.get("document") + document = cls( + raw=document_raw, + doctype=document_raw["doctype"], + name=document_raw["name"] + ) + return document + + +class Standard(Document): + ... + + +class Tool(Document): + ... + + +class CRELink(Document): + """ + Represents a link to another CRE document associated with a CRE. + + Attributes: + - id: The identifier of the linked CRE. + """ + @property + def id(self): + return self.raw.get("id") diff --git a/sdk/opencre/opencre.py b/sdk/opencre/opencre.py new file mode 100644 index 000000000..13a1a5c6a --- /dev/null +++ b/sdk/opencre/opencre.py @@ -0,0 +1,106 @@ +import requests + +from dataclasses import dataclass +from .models import CRE + + +@dataclass +class OpenCREConfig: + """ + Configuration class for OpenCRE. + + Attributes: + - HOST_URL (str): The base URL for OpenCRE. + - API_PREFIX (str): The API prefix for OpenCRE. + """ + HOST_URL: str = "https://www.opencre.org/" + API_PREFIX: str = "rest/v1/" + + +class OpenCRE: + """ + OpenCRE class for interacting with the OpenCRE API. + + Methods: + - __init__(self): Initializes an OpenCRE instance with default configuration. + - get_endpoint_url(self, endpoint_title: str): Generates the full URL for a given API endpoint title. + - perform_api_get_request(self, endpoint_title: str): Performs a GET request to the specified API endpoint. + - root_cres(self) -> list[CRE]: Retrieves a list of root CREs from the API. + - cre(self, cre_id: str) -> CRE | None: Retrieves information about a specific CRE identified by cre_id. + + Attributes: + - conf (OpenCREConfig): Configuration object containing OpenCRE settings. + """ + def __init__(self): + """ + Initializes an OpenCRE instance with default configuration. + """ + self.conf = OpenCREConfig() + + def get_endpoint_url(self, endpoint_title: str): + """ + Generates the full URL for a given API endpoint title. + + Parameters: + - endpoint_title (str): The title of the API endpoint. + + Returns: + str: The full URL for the specified API endpoint. + """ + host_url = self.conf.HOST_URL + api_prefix = self.conf.API_PREFIX + endpoint_url = f'{host_url}{api_prefix}{endpoint_title}' + return endpoint_url + + def perform_api_get_request(self, endpoint_title: str): + """ + Performs a GET request to the specified API endpoint. + + Parameters: + - endpoint_title (str): The title of the API endpoint. + + Returns: + requests.Response | None: The response object for the GET request, or None if the endpoint is not found. + """ + endpoint_url = self.get_endpoint_url(endpoint_title=endpoint_title) + response = requests.get(url=endpoint_url) + + if response.status_code == 404: + return None + + return response + + def root_cres(self) -> list[CRE]: + """ + Retrieves a list of root CREs from the API. + + Returns: + list[CRE]: A list of CRE objects representing root CREs. + """ + endpoint_title = "root_cres" + root_cres_response = self.perform_api_get_request(endpoint_title) + cres = CRE.parse_from_response(response=root_cres_response, many=True) + return cres + + def cre(self, cre_id: str) -> CRE | None: + """ + Retrieves information about a specific CRE identified by cre_id. + + Parameters: + - cre_id (str): The identifier of the CRE. + + Returns: + CRE | None: A CRE object if the CRE is found, or None if not found. + """ + endpoint_title = f"id/{cre_id}" + + if not isinstance(cre_id, str): + raise TypeError("Expected type str") + + cre_response = self.perform_api_get_request(endpoint_title) + + if cre_response is None: + return None + + cre = CRE.parse_from_response(response=cre_response) + return cre diff --git a/sdk/requirements.txt b/sdk/requirements.txt new file mode 100644 index 000000000..2c24336eb --- /dev/null +++ b/sdk/requirements.txt @@ -0,0 +1 @@ +requests==2.31.0