Repository: pablosnt/rekono-cli (language: python)

Issue:
Create specific CLI commands for specific API operations
Create CLI commands to improve user experience during Rekono API usage.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9b2fce..26ed153 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2.0.0] - 2023
+
+### Added
+
+- [**BREAKING**] Remove `install`, `update`, `uninstall` and `services` commands (https://github.com/pablosnt/rekono-cli/issues/34)
+- [**BREAKING**] Remove support for Rekono basic authentication (https://github.com/pablosnt/rekono-cli/issues/38)
+- Optimize, improve, clean and test source code (https://github.com/pablosnt/rekono-cli/issues/36)
+- Specific commands to manage specific Rekono entities (https://github.com/pablosnt/rekono-cli/issues/40)
+
+
 ## [1.1.0] - 2023-01-11
 
 ### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ed73f5c..3639e78 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,33 +1,62 @@
-Thank you for making Rekono greater.
+**Thank you for making Rekono greater!**
+
+
+## Branches
+
+**Create Pull Requests to the `develop` branch of this project**. All the Pull Requests should be reviewed and approved before been merged and after that, your code will be included in the next release.
+
+```mermaid
+gitGraph
+    commit
+    commit tag: "1.0.0"
+    branch develop
+    checkout develop
+    commit
+    commit
+    branch feature/new-contribution
+    checkout feature/new-contribution
+    commit
+    checkout develop
+    merge feature/new-contribution
+    commit
+    checkout main
+    merge develop tag: "1.1.0"
+```
+
 
-## Issues
 
-You can create different kinds of [Issues](https://github.com/pablosnt/rekono-cli/issues/new/choose) to report bugs, request new features or ask for help.
+## Development environment
 
-Please, don't report security vulnerabilities in GitHub Issues. See our [Security Policy](https://github.com/pablosnt/rekono-cli/security/policy).
+You can follow the [from source installation](https://github.com/pablosnt/rekono-cli#from-source) installation guide to prepare your development environment.
 
 
-## Contributing to Rekono
+### Unit Tests
 
-You can create Pull Requests to the `develop` branch of this project. All the Pull Requests should be reviewed and approved before been merged. After that, your code will be included on the next Rekono release.
+Unit tests can be executed using this command:
 
-In this section you can see how to achieve that and the things that you should to take into account.
+```
+# pwd: src/
+coverage run -m pytest
+```
 
-### Development environment
+New Rekono contributions should tested using unit tests.
 
-You can follow the [`From Source`](https://github.com/pablosnt/rekono-cli#from-source) installation guide to prepare your development environment.
 
 ### CI/CD
 
 This project has the following checks in _Continuous Integration_:
 
-1. `Code style`: check the source code style using the tools `mypy`, `flake8` and `eslint`.
+1. `Code style`: check the source code style using `mypy` and `flake8`.
 
-2. `SCA`: check the project dependencies to find libraries with known vulnerabilities. Software Composition Analysis.
+2. `SAST`: scan source code using `semgrep` to find vulnerabilities. This is a Static Application Security Testing.
 
-3. `Secrets scanning`: check the source code to find leaked passwords, tokens or other credentials that could be exposed in the GitHub repository.
+3. `Secrets scanning`: check the source code using `detect-secrets` to find leaked passwords, tokens or other credentials that could be exposed.
 
-**All CI/CD checks should be passed before merging any Pull Request**, so it's advised to install the pre-commit hooks in your local repositories using this commands:
+4. `Unit testing`: check if the project works executing the unit tests.
+
+5. `Snyk`: check the project dependencies to find libraries with known vulnerabilities. This is a Software Composition Analysis (SCA).
+
+**All CI/CD checks should be passed before merging any Pull Request**, so it's advised to install the pre-commit hooks in your local repositories to check your changes before commit them:
 
 ```
 # pwd: root directory
@@ -35,9 +64,12 @@ python3 -m pip install pre-commit
 pre-commit install
 ```
 
+
 ### Way of Code
 
 There are some guidelines to keep the code clean and ensure the correct working of the application:
 
-- Comment your code, specially to document the classes and methods.
+- Keep code style
+- Comment your code, specially classes and methods.
+- Make unit tests for all your code to ensure its correct working.
 - Don't include code vulnerabilities or vulnerable libraries.
diff --git a/README.md b/README.md
index f8ba862..8bb2063 100644
--- a/README.md
+++ b/README.md
@@ -20,19 +20,26 @@ Command Line Interface to make requests to the [Rekono](https://github.com/pablo
 
 ## Usage
 
-|Command|Description|
-|-------|-----------|
-|`api delete <endpoint>`|HTTP DELETE request to Rekono API|
-|`api get <endpoint> --parameter <key>=<value>`|HTTP GET request to Rekono API|
-|`api post <endpoint> --data <data>`|HTTP POST request to Rekono API|
-|`api put <endpoint> --data <data>`|HTTP PUT request to Rekono API|
+<img width="507" alt="usage" src="https://user-images.githubusercontent.com/69458381/224380037-19638197-75dc-457c-b5aa-7dcc1f9d1a4d.png">
+
+> Rekono API documentation is available in `/api/schema/swagger-ui.html` and `/api/schema/redoc/` of Rekono instances
+
+## Library usage
+
+Rekono CLI can be also used as Python 3 library, so that it's possible to create custom Rekono scripts. For example, with the following code it's possible to create a Rekono client to make custom API requests:
+
+```python
+from rekono.client.api import Rekono
+client = Rekono(url='https://127.0.0.1', token='my secret api token')           # Create Rekono client
+response = client.get('/api/tools/1/')                                          # GET request to get tool with ID 1
+```
 
 
 ## Installation
 
 ### PIP
 
-```
+```bash
 pip3 install rekono-cli
 ```
 
@@ -43,13 +50,13 @@ pip3 install rekono-cli
 
 2. Install the dependencies:
 
-    ```
+    ```bash
     pip3 install -r src/requirements.txt
     ```
 
 3. Execute the CLI:
 
-    ```
+    ```bash
     python3 src/rekono/main.py --help
     ```
 
@@ -61,20 +68,19 @@ You can use the `REKONO_TOKEN` environment variable to configure the API token f
 
 ## Support
 
-You can reach us on:
-
 <p>
-  <a href="https://github.com/pablosnt/rekono-cli/issues/new?labels=help+wanted%2C+question&template=support.md" alt="GitHub Issue">
-    <img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="64"/>
+  <a href="https://github.com/pablosnt/rekono/issues/new?labels=help+wanted%2C+question&template=support.md" alt="GitHub Issue">
+    <img src="https://github.com/fluidicon.png" width="64"/>
   </a>
   <a href="https://discord.gg/Zyduu5C7M3" alt="Discord">
     <img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg" width="64"/>
   </a>
+  <a href="mailto:rekono.project@gmail.com" alt="Mail">
+    <img src="https://www.gstatic.com/images/branding/product/2x/gmail_2020q4_512dp.png" width="64"/>
+  </a>
 </p>
 
-If you need more specific help, you can also mail rekono.project@gmail.com.
-
 
 ## License
 
-Rekono is licensed under the [GNU GENERAL PUBLIC LICENSE Version 3](../LICENSE.md)
\ No newline at end of file
+Rekono is licensed under the [GNU GENERAL PUBLIC LICENSE Version 3](../LICENSE.md)
diff --git a/src/rekono/__init__.py b/src/rekono/__init__.py
index 96e9ba9..c70ec3a 100644
--- a/src/rekono/__init__.py
+++ b/src/rekono/__init__.py
@@ -1,3 +1,3 @@
 '''Rekono CLI.'''
 
-VERSION = '1.1.0'
+VERSION = '2.0.0'
diff --git a/src/rekono/client/api.py b/src/rekono/client/api.py
index a4c3eb8..c54eab0 100644
--- a/src/rekono/client/api.py
+++ b/src/rekono/client/api.py
@@ -1,5 +1,7 @@
 '''Rekono API.'''
 
+import json
+import os
 from typing import Any, Callable, Dict, List, Optional, Union
 
 import requests
@@ -16,31 +18,27 @@ class Rekono:
     def __init__(
         self,
         url: str,
-        token: Optional[str] = None,
-        headers: Dict[str, str] = {},
-        verify: bool = False
+        token: str,
+        headers: Optional[Dict[str, str]] = None,
+        verify: bool = True
     ) -> None:
         '''Rekono API client constructor.
 
         Args:
             url (str): Base Rekono URL.
-            token (Optional[str], optional): API token for Rekono authentication. Defaults to None.
-            headers (Dict[str, str], optional): Extra HTTP request headers. Defaults to {}.
-            verify (bool, optional): Indicates if TLS verification should be performed or not. Defaults to False.
+            token (str): API token for Rekono authentication. Defaults to None.
+            headers (Optional[Dict[str, str]], optional): Extra HTTP request headers. Defaults to None.
+            verify (bool, optional): Indicates if TLS verification should be performed or not. Defaults to True.
 
         Raises:
             AuthenticationError: Authentication error during basic authentication attempt.
         '''
         self.url = url
-        self.verify = verify
-        self.headers = headers
         self.token = token
-        self.headers.update({                                                   # Configure HTTP request headers
-            'Authorization': f'Token {self.token}',
-            'Content-Type': 'application/json'
-        })
-        self.session = requests.Session()
-        # Configure retries of HTTP requests
+        self.headers = headers or {}
+        self.headers.update({'Authorization': f'Token {self.token}'})           # Configure HTTP request headers
+        self.verify = verify
+        self.session = requests.Session()                                       # Configure retries of HTTP requests
         retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429, 500, 502, 503, 504])
         self.session.mount(self.url, HTTPAdapter(max_retries=retries))
 
@@ -53,11 +51,11 @@ def _get_endpoint(self, endpoint: str) -> str:
         Returns:
             str: Valid Rekono endpoint.
         '''
-        if not endpoint.startswith('/api/'):                                    # Endpoint doesn't start by /api/
+        if not endpoint.startswith('/api/'):                                    # If endpoint doesn't start by /api/
             if endpoint.startswith('/'):
                 endpoint = endpoint[1:]
             endpoint = '/api/' + endpoint                                       # Add /api/ prefix to the endpoint
-        if not endpoint.endswith('/'):                                          # Endpoint doesn't end by slash
+        if not endpoint.endswith('/'):                                          # If endpoint doesn't end by slash
             endpoint += '/'                                                     # Add slash suffix to the endpoint
         return endpoint
 
@@ -66,7 +64,8 @@ def _request(
         method: Callable,
         endpoint: str,
         parameters: Optional[Dict[str, Any]] = None,
-        body: Optional[str] = None
+        body: Optional[str] = None,
+        files: Optional[Dict[str, Any]] = None
     ) -> Response:
         '''Perform HTTP request to Rekono API.
 
@@ -75,46 +74,54 @@ def _request(
             endpoint (str): Endpoint to call.
             parameters (Optional[Dict[str, Any]], optional): Query parameters to send. Defaults to None.
             body (Optional[str], optional): Body to send. Defaults to None.
+            files (Optional[Dict[str, Any]], optional): Files to send. Defaults to None.
 
         Raises:
             AuthenticationError: Unauthenticated, API token is invalid
-            AuthorizationError: Unauthorizated, user doesn't have required permissions.
+            AuthorizationError: Unauthorized, user hasn't required permissions.
 
         Returns:
             Response: HTTP response.
         '''
+        if files:
+            self.headers.pop('Content-Type', None)                              # If files provided, remove Content-Type
+        else:
+            self.headers['Content-Type'] = 'application/json'                   # If not, set application/json
         url = self.url + self._get_endpoint(endpoint)                           # Prepare URL to call
         try:
-            response = method(url, params=parameters, data=body, headers=self.headers, verify=self.verify)  # Attempt 1
+            # First attempt
+            response = method(url, params=parameters, data=body, files=files, headers=self.headers, verify=self.verify)
         except (ConnectionError, RetryError, Timeout):                          # Unexpected error during HTTP request
-            response = method(url, params=parameters, data=body, headers=self.headers, verify=self.verify)  # Attempt 2
-        if response.status_code == 401:
+            # Second attempt
+            response = method(url, params=parameters, data=body, files=files, headers=self.headers, verify=self.verify)
+        if response.status_code == 401:                                         # Unauthenticated
             raise AuthenticationError(response)
-        elif response.status_code == 403:
+        elif response.status_code == 403:                                       # Access Denied
             raise AuthorizationError(response)
         return response
 
     def get(
         self,
         endpoint: str,
-        parameters: Dict[str, Any] = {},
-        all_pages: bool = False
+        parameters: Optional[Dict[str, Any]] = None,
+        pagination: bool = False
     ) -> Union[List[Response], Response]:
         '''GET request to Rekono API.
 
         Args:
             endpoint (str): Endpoint to call.
             parameters (Optional[Dict[str, Any]], optional): Query parameters to send. Defaults to {}.
-            all_pages (bool, optional): Iteration over all API pages is required. Defaults to False.
+            pagination (bool, optional): Enables iteration over all API pages. Defaults to False.
 
         Raises:
             AuthenticationError: Unauthenticated, API token is invalid
             AuthorizationError: Unauthorizated, user doesn't have required permissions.
 
         Returns:
-            Union[List[Response], Response]: HTTP responses if all pages is enabled or one HTTP response if not.
+            Union[List[Response], Response]: HTTP responses if pagination is enabled or one HTTP response if not.
         '''
-        if not all_pages:                                                       # All pages is disabled
+        parameters = parameters or {}
+        if not pagination:                                                      # Pagination is disabled
             return self._request(self.session.get, endpoint, parameters=parameters)     # Perform only one GET request
         page = 1                                                                # Start page number
         size = 100                                                              # Default page size (max is 1000)
@@ -132,12 +139,13 @@ def get(
                 break
         return responses
 
-    def post(self, endpoint: str, body: Optional[str] = None) -> Response:
+    def post(self, endpoint: str, body: Optional[str] = None, filepath: Optional[str] = None) -> Response:
         '''POST request to Rekono API.
 
         Args:
             endpoint (str): Endpoint to call.
             body (Optional[str], optional): Body to send. Defaults to None.
+            filepath (Optional[str], optional): File to send. Defaults to None.
 
         Raises:
             AuthenticationError: Unauthenticated, API token is invalid
@@ -146,7 +154,12 @@ def post(self, endpoint: str, body: Optional[str] = None) -> Response:
         Returns:
             Response: HTTP response.
         '''
-        return self._request(self.session.post, endpoint, body=body)            # Perform POST request
+        if filepath and os.path.isfile(filepath):                               # If filepath is provided
+            with open(filepath, 'rb') as file:                                  # Read file
+                return self._request(                                           # Perform POST request
+                    self.session.post, endpoint, body=json.loads(body) if body else None, files={'file': file}
+                )
+        return self._request(self.session.post, endpoint, body=body)            # Perform POST request without files
 
     def put(self, endpoint: str, body: Optional[str] = None) -> Response:
         '''PUT request to Rekono API.
diff --git a/src/rekono/client/enums.py b/src/rekono/client/enums.py
new file mode 100644
index 0000000..0d4c948
--- /dev/null
+++ b/src/rekono/client/enums.py
@@ -0,0 +1,48 @@
+'''Rekono enums.'''
+
+from enum import Enum
+
+
+class AuthenticationType(Enum):
+    '''Supported authentication types.'''
+
+    BASIC = 'Basic'
+    BEARER = 'Bearer'
+    COOKIE = 'Cookie'
+    DIGEST = 'Digest'
+    JWT = 'JWT'
+    NTLM = 'NTLM'
+
+
+class UserRole(Enum):
+    '''User role names.'''
+
+    ADMIN = 'Admin'
+    AUDITOR = 'Auditor'
+    READER = 'Reader'
+
+
+class IntensityRank(Enum):
+    '''Intensity ranks.'''
+
+    SNEAKY = 'Sneaky'
+    LOW = 'Low'
+    NORMAL = 'Normal'
+    HARD = 'Hard'
+    INSANE = 'Insane'
+
+
+class TimeUnit(Enum):
+    '''Time units supported for Task scheduling and repeating configuration.'''
+
+    MINUTES = 'Minutes'
+    HOURS = 'Hours'
+    DAYS = 'Days'
+    WEEKS = 'Weeks'
+
+
+class WordlistType(Enum):
+    '''Wordlist type names.'''
+
+    ENDPOINT = 'Endpoint'
+    SUBDOMAIN = 'Subdomain'
diff --git a/src/rekono/client/exceptions.py b/src/rekono/client/exceptions.py
index 2d3ebf9..fd36e84 100644
--- a/src/rekono/client/exceptions.py
+++ b/src/rekono/client/exceptions.py
@@ -39,8 +39,7 @@ def __init__(self, response: Response) -> None:
         Args:
             response (Response): Http response that causes the error.
         '''
-        self.message = 'Unauthorized: Invalid Rekono API token'
-        super().__init__(self.message, response)
+        super().__init__('Unauthorized: Invalid Rekono API token', response)
 
 
 class AuthorizationError(RekonoException):
@@ -52,5 +51,4 @@ def __init__(self, response: Response) -> None:
         Args:
             response (Response): Http response that causes the error.
         '''
-        self.message = 'Unauthorized: User doesn\'t have required permissions to perform this action'
-        super().__init__(self.message, response)
+        super().__init__('Unauthorized: User hasn\'t required permissions to perform this action', response)
diff --git a/src/rekono/commands/api.py b/src/rekono/commands/api.py
index 03dbfb0..6fd9fc3 100644
--- a/src/rekono/commands/api.py
+++ b/src/rekono/commands/api.py
@@ -1,10 +1,10 @@
-'''Command to perform standard Rekono API requests.'''
+'''Command to make custom Rekono API requests.'''
 
 import click
 
-from rekono.framework.commands import RekonoApiCommand
+from rekono.framework.commands.api import ApiCommand
 
 
-@click.group('api', cls=RekonoApiCommand, help='Perform Rekono API requests')
+@click.group('api', cls=ApiCommand, help='Make custom Rekono API requests')
 def api():
-    '''Perform Rekono API requests.'''
+    '''Make custom Rekono API requests.'''
diff --git a/src/rekono/commands/authentications.py b/src/rekono/commands/authentications.py
new file mode 100644
index 0000000..e551254
--- /dev/null
+++ b/src/rekono/commands/authentications.py
@@ -0,0 +1,33 @@
+'''CLI command to manage Authentication entities.'''
+
+import click
+
+from rekono.client.enums import AuthenticationType
+from rekono.framework.commands.entity import EntityCommand
+
+
+class AuthenticationsCommand(EntityCommand):
+    '''CLI command to manage Authentication entities.'''
+
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all wordlists or one if ID is provided',
+        'create': 'Create wordlist',
+        'update': 'Update wordlist',
+        'delete': 'Delete wordlist',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-p', '--target-port', 'target_port', required=True, type=int, help='Related target port ID'),
+        click.option('-n', '--name', 'name', required=True, type=str, help='Authentication name'),
+        click.option('-c', '--credential', 'credential', required=True, type=str, help='Authentication secret value'),
+        click.option(
+            '-t', '--type', 'auth_type',
+            required=False, default=AuthenticationType.BASIC.value,
+            type=click.Choice([t.value for t in AuthenticationType]),
+            help='Authentication type'
+        )
+    ]
+
+
+@click.group('authentications', cls=AuthenticationsCommand, help='Manage target authentications')
+def authentications():
+    '''Manage target authentications.'''
diff --git a/src/rekono/commands/configurations.py b/src/rekono/commands/configurations.py
new file mode 100644
index 0000000..9e4e066
--- /dev/null
+++ b/src/rekono/commands/configurations.py
@@ -0,0 +1,19 @@
+'''CLI command to manage Configuration entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class ConfigurationsCommand(EntityCommand):
+    '''CLI command to manage Configuration entities.'''
+
+    commands = ['get']                                                          # CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all configurations or one if ID is provided',
+    }
+
+
+@click.group('configurations', cls=ConfigurationsCommand, help='Get configurations')
+def configurations():
+    '''Get configurations.'''
diff --git a/src/rekono/commands/executions.py b/src/rekono/commands/executions.py
new file mode 100644
index 0000000..6d4588f
--- /dev/null
+++ b/src/rekono/commands/executions.py
@@ -0,0 +1,19 @@
+'''CLI command to manage Execution entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class ExecutionsCommand(EntityCommand):
+    '''CLI command to manage Execution entities.'''
+
+    commands = ['get']                                                          # CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all executions or one if ID is provided',
+    }
+
+
+@click.group('executions', cls=ExecutionsCommand, help='Get executions')
+def executions():
+    '''Get executions'''
diff --git a/src/rekono/commands/findings.py b/src/rekono/commands/findings.py
new file mode 100644
index 0000000..5799f61
--- /dev/null
+++ b/src/rekono/commands/findings.py
@@ -0,0 +1,72 @@
+'''CLI command to manage findings entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class FindingsCommand(EntityCommand):
+    '''CLI command to manage findings entities.'''
+
+    commands = ['get', 'enable', 'disable']                                     # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_entity',
+        'disable': 'delete_entity',
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all findings or one if ID is provided',
+        'enable': 'Enable finding',
+        'disable': 'Disable finding',
+    }
+
+
+@click.group('credentials', cls=FindingsCommand, help='Manage credentials')
+def credentials():
+    '''Manage credentials.'''
+
+
+@click.group('exploits', cls=FindingsCommand, help='Manage exploits')
+def exploits():
+    '''Manage exploits.'''
+
+
+@click.group('hosts', cls=FindingsCommand, help='Manage hosts')
+def hosts():
+    '''Manage hosts.'''
+
+
+@click.group('paths', cls=FindingsCommand, help='Manage paths')
+def paths():
+    '''Manage paths.'''
+
+
+@click.group('ports', cls=FindingsCommand, help='Manage ports')
+def ports():
+    '''Manage ports.'''
+
+
+@click.group('technologies', cls=FindingsCommand, help='Manage technologies')
+def technologies():
+    '''Manage technologies.'''
+
+
+@click.group('vulnerabilities', cls=FindingsCommand, help='Manage vulnerabilities')
+def vulnerabilities():
+    '''Manage vulnerabilities.'''
+
+
+class OSINTCommand(FindingsCommand):
+    '''CLI command to manage OSINT entities.'''
+
+    commands = ['get', 'enable', 'disable', 'target']                           # Supported CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all findings or one if ID is provided',
+        'enable': 'Enable finding',
+        'disable': 'Disable finding',
+        'target': 'Create target from OSINT data',
+    }
+
+
+@click.group('osint', cls=OSINTCommand, help='Manage OSINT findings')
+def osint():
+    '''Manage OSINT findings.'''
diff --git a/src/rekono/commands/processes.py b/src/rekono/commands/processes.py
new file mode 100644
index 0000000..27d8878
--- /dev/null
+++ b/src/rekono/commands/processes.py
@@ -0,0 +1,27 @@
+'''CLI command to manage Process entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import tags_option
+
+
+class ProcessesCommand(EntityCommand):
+    '''CLI command to manage Process entities.'''
+
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all processes or one if ID is provided',
+        'create': 'Create process',
+        'update': 'Update process',
+        'delete': 'Delete process'
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-n', '--name', 'name', required=True, type=str, help='Process name'),
+        click.option('-d', '--description', 'description', required=True, type=str, help='Process description'),
+        tags_option
+    ]
+
+
+@click.group('processes', cls=ProcessesCommand, help='Manage processes')
+def processes():
+    '''Manage processes.'''
diff --git a/src/rekono/commands/profile.py b/src/rekono/commands/profile.py
new file mode 100644
index 0000000..188c6fc
--- /dev/null
+++ b/src/rekono/commands/profile.py
@@ -0,0 +1,92 @@
+'''CLI command to manage user profile.'''
+
+import json
+from typing import List
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import json_option
+
+
+class ProfileCommand(EntityCommand):
+    '''CLI command to manage user profile.'''
+
+    commands = ['get', 'telegram']                                              # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_profile',
+        'telegram': 'link_telegram_bot'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get current user profile',
+        'telegram': 'Link Telegram bot to current user'
+    }
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @json_option
+    def get_profile(
+        ctx: click.Context,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ) -> None:
+        '''Get current user profile.
+
+        Args:
+            ctx (click.Context): Click context.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        ctx.invoke(
+            ProfileCommand.get, endpoint='/api/profile/',
+            url=url, headers=headers, no_verify=no_verify, parameters=[], pagination=False,
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet, json_output=json_output
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @click.option('-t', '--token', 'token', required=True, type=str, help='Token provided by Telegram bot')
+    def link_telegram_bot(
+        ctx: click.Context,
+        token: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ) -> None:
+        '''Link user account to Telegram chat.
+
+        Args:
+            ctx (click.Context): Click context.
+            token (str): Temporal token provided by Rekono bot.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        ctx.invoke(
+            ProfileCommand.post, endpoint='/api/profile/telegram-token/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps({'otp': token}),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet, json_output=None
+        )
+
+
+@click.group('profile', cls=ProfileCommand, help='Manage user profile')
+def profile():
+    '''Manage user profile.'''
diff --git a/src/rekono/commands/projects.py b/src/rekono/commands/projects.py
new file mode 100644
index 0000000..70b6d5d
--- /dev/null
+++ b/src/rekono/commands/projects.py
@@ -0,0 +1,112 @@
+'''CLI command to manage Project entities.'''
+
+import json
+from typing import List
+
+import click
+
+from rekono.framework.arguments import id_mandatory_argument
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import tags_option
+
+
+class ProjectsCommand(EntityCommand):
+    '''CLI command to manage Project entities.'''
+
+    commands = ['get', 'create', 'update', 'delete', 'add-member', 'remove-member']                 # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_entity',
+        'create': 'post_entity',
+        'update': 'put_entity',
+        'delete': 'delete_entity',
+        'add-member': 'add_member',
+        'remove-member': 'remove_member'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all projects or one if ID is provided',
+        'create': 'Create project',
+        'update': 'Update project',
+        'delete': 'Delete project',
+        'add-member': 'Add member to project',
+        'remove-member': 'Remove member from project'
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-n', '--name', 'name', required=True, type=str, help='Project name'),
+        click.option('-d', '--description', 'description', required=True, type=str, help='Project description'),
+        tags_option
+    ]
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    @click.option('-u', '--user', 'user', required=True, type=int, help='User ID')
+    def add_member(
+        ctx: click.Context,
+        id: int,
+        user: int,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ) -> None:
+        '''Add user as member to project.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): Project ID to remove user from.
+            user (int): User ID to add to the project as member.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        ctx.invoke(
+            ProjectsCommand.post, endpoint=f'/api/projects/{id}/members/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps({'user': user}),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    @click.option('-u', '--user', 'user', required=True, type=int, help='User ID')
+    def remove_member(
+        ctx: click.Context,
+        id: int,
+        user: int,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ) -> None:
+        '''Remove member from project.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): Project ID to remove user from.
+            user (int): User ID to add to the project as member.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        ctx.invoke(
+            ProjectsCommand.delete, endpoint=f'/api/projects/{id}/members/{user}/',
+            url=url, headers=headers, no_verify=no_verify,
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet
+        )
+
+
+@click.group('projects', cls=ProjectsCommand, help='Manage projects')
+def projects():
+    '''Manage projects.'''
diff --git a/src/rekono/commands/settings.py b/src/rekono/commands/settings.py
new file mode 100644
index 0000000..eb21700
--- /dev/null
+++ b/src/rekono/commands/settings.py
@@ -0,0 +1,57 @@
+'''CLI command to manage system settings.'''
+
+from typing import List
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import json_option
+
+
+class SettingsCommand(EntityCommand):
+    '''CLI command to manage system settings.'''
+
+    commands = ['get']                                                          # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_settings'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get system settings'
+    }
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @json_option
+    def get_settings(
+        ctx: click.Context,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ) -> None:
+        '''Get current system settings.
+
+        Args:
+            ctx (click.Context): Click context.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        ctx.invoke(
+            SettingsCommand.get, endpoint='/api/system/1/',
+            url=url, headers=headers, no_verify=no_verify, parameters=[], pagination=False,
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet, json_output=json_output
+        )
+
+
+@click.group('settings', cls=SettingsCommand, help='Get system settings')
+def settings():
+    '''Get system settings.'''
diff --git a/src/rekono/commands/steps.py b/src/rekono/commands/steps.py
new file mode 100644
index 0000000..7031ef7
--- /dev/null
+++ b/src/rekono/commands/steps.py
@@ -0,0 +1,27 @@
+'''CLI command to manage Step entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class StepsCommand(EntityCommand):
+    '''CLI command to manage Step entities.'''
+
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all steps or one if ID is provided',
+        'create': 'Create step',
+        'update': 'Update step',
+        'delete': 'Delete step',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-p', '--process', 'process', required=True, type=int, help='Process ID'),
+        click.option('-t', '--tool', 'tool_id', required=True, type=int, help='Tool ID'),
+        click.option('-c', '--configuration', 'configuration_id', required=True, type=int, help='Configuration ID'),
+        click.option('--priority', 'priority', required=False, default=1, type=int, help='Step priority within process')
+    ]
+
+
+@click.group('steps', cls=StepsCommand, help='Manage steps')
+def steps():
+    '''Manage steps.'''
diff --git a/src/rekono/commands/target_ports.py b/src/rekono/commands/target_ports.py
new file mode 100644
index 0000000..ee03608
--- /dev/null
+++ b/src/rekono/commands/target_ports.py
@@ -0,0 +1,25 @@
+'''CLI command to manage TargetPort entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class TargetPortsCommand(EntityCommand):
+    '''CLI command to manage TargetPort entities.'''
+
+    commands = ['get', 'create', 'delete']                                      # CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all target ports or one if ID is provided',
+        'create': 'Create target port',
+        'delete': 'Delete target port',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-t', '--target', 'target', required=True, type=int, help='Target ID'),
+        click.option('-p', '--port', 'port', required=True, type=int, help='Port number')
+    ]
+
+
+@click.group('target-ports', cls=TargetPortsCommand, help='Manage target ports')
+def target_ports():
+    '''Manage target ports.'''
diff --git a/src/rekono/commands/targets.py b/src/rekono/commands/targets.py
new file mode 100644
index 0000000..995f9c5
--- /dev/null
+++ b/src/rekono/commands/targets.py
@@ -0,0 +1,30 @@
+'''CLI command to manage Target entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class TargetsCommand(EntityCommand):
+    '''CLI command to manage Target entities.'''
+
+    commands = ['get', 'create', 'delete']                                      # CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all targets or one if ID is provided',
+        'create': 'Create target',
+        'delete': 'Delete target',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-p', '--project', 'project', required=True, type=int, help='Project ID'),
+        click.option('-t', '--target', 'target', required=True, type=str, help='Target address'),
+        click.option(
+            '-d', '--dd-engagement', 'defectdojo_engagement_id',
+            required=False, default=None, type=int,
+            help='Engagement ID in Defect-Dojo'
+        )
+    ]
+
+
+@click.group('targets', cls=TargetsCommand, help='Manage targets')
+def targets():
+    '''Manage targets.'''
diff --git a/src/rekono/commands/tasks.py b/src/rekono/commands/tasks.py
new file mode 100644
index 0000000..55f0288
--- /dev/null
+++ b/src/rekono/commands/tasks.py
@@ -0,0 +1,111 @@
+'''CLI command to manage Task entities.'''
+
+import json
+from typing import Any, List
+
+import click
+
+from rekono.client.enums import IntensityRank, TimeUnit
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import json_option
+
+
+class TasksCommand(EntityCommand):
+    '''CLI command to manage Task entities.'''
+
+    commands = ['get', 'create', 'cancel', 'repeat']                            # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_entity',
+        'create': 'post_entity',
+        'cancel': 'delete_entity'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all tasks or one if ID is provided',
+        'create': 'Create task',
+        'cancel': 'Cancel task execution',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-t', '--target', 'target_id', required=True, type=int, help='Process ID'),
+        click.option('-p', '--process', 'process_id', required=False, default=None, type=int, help='Process ID'),
+        click.option('--tool', 'tool_id', required=False, default=None, type=int, help='Tool ID'),
+        click.option(
+            '-c', '--configuration', 'configuration_id',
+            required=False, default=None,
+            type=int, help='Configuration ID'
+        ),
+        click.option(
+            '-i', '--intensity', 'intensity_rank',
+            required=False, default=IntensityRank.NORMAL.value,
+            type=click.Choice([t.value for t in IntensityRank]),
+            help='Intensity rank'
+        ),
+        click.option(
+            '-sat', '--scheduled-at', 'scheduled_at',
+            required=False, type=click.DateTime(formats=['%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S']),
+            help='Exactly time to scheduled task at'
+        ),
+        click.option(
+            '-sin', '--scheduled-in', 'scheduled_in',
+            required=False, type=int, help='Schedule task to some time later'
+        ),
+        click.option(
+            '-stu', '--scheduled-time-unit', 'scheduled_time_unit',
+            required=False, type=click.Choice([t.value for t in TimeUnit]),
+            help='Time unit to apply in scheduling'
+        ),
+        click.option(
+            '-rin', '--repeat-in', 'repeat_in',
+            required=False, type=int, help='Repeat task periodically after some time'
+        ),
+        click.option(
+            '-rtu', '--repeat-time-unit', 'repeat_time_unit',
+            required=False, type=click.Choice([t.value for t in TimeUnit]),
+            help='Time unit to apply in repeating'
+        ),
+        click.option('-w', '--wordlist', 'wordlists', multiple=True, required=False, type=int, help='Wordlist ID')
+    ]
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @json_option
+    def post_entity(
+        ctx: click.Context,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str,
+        **kwargs: Any
+    ) -> None:
+        '''POST request to create specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+            kwargs (Any): Variable fields that will be sent as body
+        '''
+        for no_null_field in ['process_id', 'tool_id', 'configuration_id']:
+            if no_null_field in kwargs and not kwargs.get(no_null_field):       # Empty value
+                kwargs.pop(no_null_field)                                       # Remove empty values from body data
+        # Set scheduled_at field as ISO format
+        kwargs['scheduled_at'] = kwargs['scheduled_at'].astimezone().isoformat() if kwargs.get('scheduled_at') else None
+        ctx.invoke(
+            TasksCommand.post, endpoint='/api/tasks/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps(kwargs),
+            show_headers=show_headers, only_show_status_code=only_show_status_code,
+            quiet=quiet, json_output=json_output
+        )
+
+
+@click.group('tasks', cls=TasksCommand, help='Manage tasks')
+def tasks():
+    '''Manage tasks.'''
diff --git a/src/rekono/commands/tools.py b/src/rekono/commands/tools.py
new file mode 100644
index 0000000..c90d00b
--- /dev/null
+++ b/src/rekono/commands/tools.py
@@ -0,0 +1,19 @@
+'''CLI command to manage Tool entities.'''
+
+import click
+
+from rekono.framework.commands.entity import EntityCommand
+
+
+class ToolsCommand(EntityCommand):
+    '''CLI command to manage Tool entities.'''
+
+    commands = ['get']                                                          # CLI commands
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all tools or one if ID is provided',
+    }
+
+
+@click.group('tools', cls=ToolsCommand, help='Get tools')
+def tools():
+    '''Get tools.'''
diff --git a/src/rekono/commands/users.py b/src/rekono/commands/users.py
new file mode 100644
index 0000000..74b6f36
--- /dev/null
+++ b/src/rekono/commands/users.py
@@ -0,0 +1,117 @@
+'''CLI command to manage User entities.'''
+
+import json
+from typing import List
+
+import click
+
+from rekono.client.enums import UserRole
+from rekono.framework.arguments import id_mandatory_argument
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import json_option
+
+user_role_option = click.option(                                                # Role CLI option
+    '-r', '--role', 'role',
+    required=False, default=UserRole.READER.value,
+    type=click.Choice([t.value for t in UserRole]),
+    help='User role'
+)
+
+
+class UsersCommand(EntityCommand):
+    '''CLI command to manage User entities.'''
+
+    commands = ['get', 'delete', 'enable', 'role', 'invite']                    # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_entity',
+        'delete': 'delete_entity',
+        'role': 'update_role',
+        'invite': 'invite'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all users or one if ID is provided',
+        'delete': 'Delete user',
+        'role': 'Update user role',
+        'invite': 'Invite new user'
+    }
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    @user_role_option
+    @json_option
+    def update_role(
+        ctx: click.Context,
+        id: int,
+        role: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ) -> None:
+        '''Update user role.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): User ID to update.
+            role (str): Role to asssin to the user.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        ctx.invoke(
+            UsersCommand.put, endpoint=f'/api/users/{id}/role/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps({'role': role}),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet, json_output=json_output
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @click.option('-e', '--email', 'email', required=True, type=str, help='User email to send invitation')
+    @user_role_option
+    @json_option
+    def invite(
+        ctx: click.Context,
+        email: str,
+        role: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ) -> None:
+        '''Invite new user.
+
+        Args:
+            ctx (click.Context): Click context.
+            email (str): User email to send invitation to.
+            role (str): Role to asssin to the user.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        ctx.invoke(
+            UsersCommand.post, endpoint='/api/users/invite/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps({'email': email, 'role': role}),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet, json_output=json_output
+        )
+
+
+@click.group('users', cls=UsersCommand, help='Manage users')
+def users():
+    '''Manage users.'''
diff --git a/src/rekono/commands/wordlists.py b/src/rekono/commands/wordlists.py
new file mode 100644
index 0000000..ade101f
--- /dev/null
+++ b/src/rekono/commands/wordlists.py
@@ -0,0 +1,73 @@
+'''CLI command to manage Wordlist entities.'''
+
+import json
+from typing import Any, List
+
+import click
+
+from rekono.client.enums import WordlistType
+from rekono.framework.commands.entity import EntityCommand
+from rekono.framework.options import json_option
+
+
+class WordlistsCommand(EntityCommand):
+    '''CLI command to manage Wordlist entities.'''
+
+    help_messages = {                                                           # Help messages for each command
+        'get': 'Get all wordlists or one if ID is provided',
+        'create': 'Create wordlist',
+        'update': 'Update wordlist',
+        'delete': 'Delete wordlist',
+    }
+    entity_options = [                                                          # Specific options for post and put
+        click.option('-n', '--name', 'name', required=True, type=str, help='Wordlist name'),
+        click.option(
+            '-t', '--type', 'type',
+            required=False, default=WordlistType.ENDPOINT.value,
+            type=click.Choice([t.value for t in WordlistType]),
+            help='Wordlist name'
+        )
+    ]
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @click.option('-f', '--file', 'filepath', required=True, type=click.Path(exists=True), help='Wordlists file')
+    @json_option
+    def post_entity(
+        ctx: click.Context,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str,
+        **kwargs: Any
+    ) -> None:
+        '''POST request to create specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            filepath (str): Filepath to upload as wordlist file.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+            kwargs (Any): Variable fields that will be sent as body
+        '''
+        filepath = kwargs.pop('filepath')
+        ctx.invoke(
+            WordlistsCommand.post, endpoint='/api/wordlists/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps(kwargs),
+            show_headers=show_headers, only_show_status_code=only_show_status_code,
+            quiet=quiet, json_output=json_output, filepath=filepath
+        )
+
+
+@click.group('wordlists', cls=WordlistsCommand, help='Manage wordlists')
+def wordlists():
+    '''Manage wordlists.'''
diff --git a/src/rekono/framework/__init__.py b/src/rekono/framework/__init__.py
index 0b6f92c..f4376ca 100644
--- a/src/rekono/framework/__init__.py
+++ b/src/rekono/framework/__init__.py
@@ -1,1 +1,1 @@
-'''Rekono CLI framework that includes base features, code and design.'''
+'''Rekono CLI framework that includes base commands, options and arguments.'''
diff --git a/src/rekono/framework/arguments.py b/src/rekono/framework/arguments.py
index 3ca4423..4c817ba 100644
--- a/src/rekono/framework/arguments.py
+++ b/src/rekono/framework/arguments.py
@@ -1,5 +1,9 @@
-'''Definition of CLI arguments.'''
+'''Definition of base CLI arguments used by multiple commands.'''
 
 import click
 
 endpoint_argument = click.argument('endpoint', type=str, nargs=1)               # Endpoint argument
+
+id_optional_argument = click.argument('id', type=int, nargs=1, required=False)  # Id optional argument
+
+id_mandatory_argument = click.argument('id', type=int, nargs=1)                 # Id mandatory argument
diff --git a/src/rekono/framework/commands.py b/src/rekono/framework/commands.py
deleted file mode 100644
index 6c34f0e..0000000
--- a/src/rekono/framework/commands.py
+++ /dev/null
@@ -1,370 +0,0 @@
-'''Definition of base features for CLI commands.'''
-
-import json
-import os
-import sys
-from typing import Any, Dict, List, Optional, Union
-from urllib.parse import urlparse
-
-import click
-import requests
-from requests.models import Response
-
-from rekono.client.api import Rekono
-from rekono.framework.arguments import endpoint_argument
-from rekono.framework.options import (all_pages_option, body_option,
-                                      headers_option, json_option,
-                                      no_verify_option, parameters_option,
-                                      quiet_option, show_headers_option,
-                                      status_code_option, url_option)
-
-
-class RekonoApiCommand(click.MultiCommand):
-    '''Base Rekono CLI command.'''
-
-    api_token_env = 'REKONO_TOKEN'                                              # Environment variable to set API token
-    commands = ['get', 'post', 'put', 'delete']                                 # List of CLI commands supported
-
-    def list_commands(self, ctx: click.Context) -> List[str]:
-        '''Return list of CLI commands.
-
-        Args:
-            ctx (click.Context): Click context.
-
-        Returns:
-            List[str]: List of CLI commands.
-        '''
-        return self.commands
-
-    def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
-        '''Get CLI command by name.
-
-        Args:
-            ctx (click.Context): Click context.
-            cmd_name (str): Command name.
-
-        Returns:
-            Optional[click.Command]: Click command.
-        '''
-        return getattr(self, cmd_name) if hasattr(self, cmd_name) else None
-
-    @staticmethod
-    def _get_url(url: str) -> str:
-        '''Get valid Rekono base URL from the user provided value.
-
-        Args:
-            url (str): User provided URL.
-
-        Returns:
-            str: Valid Rekono base URL.
-        '''
-        parser = urlparse(url)                                                  # Parse provided URL
-        if not parser.netloc:                                                   # Invalid URL
-            click.echo(click.style('URL is invalid', fg='red'), err=True, color=True)
-            url = click.prompt('URL', type=str)                                 # Ask user for base URL
-            return RekonoApiCommand._get_url(url)                               # Retry URL validation
-        return f'{parser.scheme or "https"}://{parser.netloc}'
-
-    @staticmethod
-    def _parse_key_value_params(items: List[str]) -> Dict[str, str]:
-        '''Parse key=value options.
-
-        Args:
-            items (List[str]): List of key=value values.
-
-        Returns:
-            Dict[str, str]: Dictionary with all keys and values.
-        '''
-        data = {}
-        for item in items or []:
-            if '=' in item:                                                     # Check separator
-                key, value = item.split('=', 1)                                 # Get key and value
-                data[key.strip()] = value.strip()
-        return data
-
-    @staticmethod
-    def _get_body(body: Optional[str]) -> Optional[str]:
-        '''Validate body value to be sent via HTTP.
-
-        Args:
-            body (Optional[str]): Body value.
-
-        Returns:
-            Optional[str]: Validated body value.
-        '''
-        if body:
-            try:
-                json.loads(body)                                                # Try to parse body value
-            except json.decoder.JSONDecodeError:                                # Invalid body value
-                click.echo(click.style('Invalid JSON format for body value', fg='red'), err=True, color=True)
-                sys.exit(1)
-        return body
-
-    @staticmethod
-    def _get_endpoint(endpoint: str) -> str:
-        '''Get valid endpoint value without parameters or other URL information.
-
-        Args:
-            endpoint (str): Endpoint value provided by user.
-
-        Returns:
-            str: Valid endpoint value.
-        '''
-        return urlparse(endpoint).path
-
-    @staticmethod
-    def _rekono_factory(url: str, no_verify: bool = False, headers: List[str] = []) -> Rekono:
-        '''Create Rekono client entity.
-
-        Args:
-            url (str): Base Rekono URL.
-            no_verify (bool, optional): Disable TLS validation. Defaults to False.
-            headers (List[str], optional): Extra HTTP request headers. Defaults to [].
-
-        Returns:
-            Rekono: Rekono API client.
-        '''
-        token = os.getenv(RekonoApiCommand.api_token_env)                       # Get API token from environment
-        if not token:                                                           # API token is not provided
-            token = click.prompt('API token', type=str, hide_input=True)        # Ask for API token
-        return Rekono(                                                          # Create Rekono API client
-            RekonoApiCommand._get_url(url),                                     # Get valid Rekono URL
-            token=token,
-            headers=RekonoApiCommand._parse_key_value_params(headers),          # Get HTTP headers
-            verify=not no_verify
-        )
-
-    @staticmethod
-    def _get_data_from_responses(responses: List[Response]) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
-        '''Get data from Rekono API responses.
-
-        Args:
-            responses (List[Response]): List of Rekono API responses.
-
-        Returns:
-            Union[List[Dict[str, Any]], Dict[str, Any]]: Data returned by Rekono.
-        '''
-        content = []
-        for response in responses:                                              # For each response
-            try:
-                body = response.json()                                          # Parse JSON body from response
-            except requests.exceptions.JSONDecodeError:
-                continue
-            if isinstance(body, dict):                                          # Response body is a dictionary
-                body = body.get('results', body)                                # Get results field if it exists
-            if isinstance(body, list):                                          # Content is a list
-                content.extend(body)                                            # Save content
-            elif isinstance(body, dict):                                        # Content is a dictionary
-                if len(responses) == 1:                                         # Only one response
-                    return body                                                 # Return response content
-                content.append(body)                                            # Save content
-        return content
-
-    @staticmethod
-    def _display_responses(
-        responses: List[Response],
-        show_headers: bool,
-        just_show_status_code: bool,
-        quiet: bool
-    ) -> None:
-        '''Display Rekono API responses via standard output.
-
-        Args:
-            responses (List[Response]): Rekono API responses.
-            show_headers (bool): Display HTTP response headers.
-            just_show_status_code (bool): Just display HTTP response status code.
-            quiet (bool): Don't display anything from response.
-        '''
-        if quiet:                                                               # No content should be displayed
-            return
-        if just_show_status_code or show_headers:                               # Headers or status should be displayed
-            for response in responses:                                          # For each response
-                if just_show_status_code:                                       # Just display status code
-                    click.echo(response.status_code)
-                elif show_headers:                                              # Show response headers
-                    click.echo()
-                    # Display HTTP request and response summary
-                    click.echo(f'{response.request.method} {response.request.path_url} {response.status_code}')
-                    for header, value in response.headers.items():
-                        click.echo(f'{header}: {value}')                        # Display HTTP response headers
-                    click.echo()
-                    data = RekonoApiCommand._get_data_from_responses([response])    # Get content from response
-                    click.echo(json.dumps(data, ensure_ascii=True, indent=4))   # Display content via standard output
-        else:                                                                   # Standard display options
-            data = RekonoApiCommand._get_data_from_responses(responses)         # Get content from responses
-            click.echo(json.dumps(data, ensure_ascii=True, indent=4))           # Display content via standard output
-
-    @staticmethod
-    def _save_output(responses: List[Response], filepath: Optional[str]) -> None:
-        '''Save responses content in JSON file.
-
-        Args:
-            responses (List[Response]): Rekono API responses.
-            filepath (Optional[str]): Filepath to the JSON file where content should be saved.
-        '''
-        if not filepath:                                                        # JSON filepath is provided
-            return
-        data = RekonoApiCommand._get_data_from_responses(responses)             # Get data from responses
-        with open(filepath, 'w', encoding='utf-8') as file:                     # Open JSON file
-            json.dump(data, file, ensure_ascii=True, indent=4)                  # Write content in JSON file
-
-    @staticmethod
-    @click.command(help='GET request to Rekono API')
-    @endpoint_argument
-    @url_option
-    @headers_option
-    @parameters_option
-    @no_verify_option
-    @all_pages_option
-    @show_headers_option
-    @status_code_option
-    @quiet_option
-    @json_option
-    def get(
-        endpoint: str,
-        url: str,
-        headers: List[str],
-        parameters: List[str],
-        no_verify: bool,
-        all_pages: bool,
-        show_headers: bool,
-        just_show_status_code: bool,
-        quiet: bool,
-        json_output: str
-    ):
-        '''GET request to Rekono API.
-
-        Args:
-            endpoint (str): Endpoint to call.
-            url (str): Rekono base URL.
-            headers (List[str]): HTTP headers to send in key=value format.
-            parameters (List[str]): HTTP query parameters to send in key=value format.
-            no_verify (bool): Disable TLS validation.
-            all_pages (bool): Enable iteration over all API pages.
-            show_headers (bool): Display HTTP response headers.
-            just_show_status_code (bool): Just display HTTP response status code.
-            quiet (bool): Don't display anything from response.
-            json_output (str): Filepath to the JSON file where content should be saved.
-        '''
-        client = RekonoApiCommand._rekono_factory(url, no_verify, headers)
-        response_or_responses = client.get(
-            RekonoApiCommand._get_endpoint(endpoint),
-            parameters=RekonoApiCommand._parse_key_value_params(parameters),
-            all_pages=all_pages
-        )
-        responses = response_or_responses if isinstance(response_or_responses, list) else [response_or_responses]
-        RekonoApiCommand._display_responses(responses, show_headers, just_show_status_code, quiet)
-        RekonoApiCommand._save_output(responses, json_output)
-
-    @staticmethod
-    @click.command(help='POST request to Rekono API')
-    @endpoint_argument
-    @url_option
-    @headers_option
-    @body_option
-    @no_verify_option
-    @show_headers_option
-    @status_code_option
-    @quiet_option
-    @json_option
-    def post(
-        endpoint: str,
-        url: str,
-        headers: List[str],
-        body: str,
-        no_verify: bool,
-        show_headers: bool,
-        just_show_status_code: bool,
-        quiet: bool,
-        json_output: str
-    ):
-        '''POST request to Rekono API.
-
-        Args:
-            endpoint (str): Endpoint to call.
-            url (str): Rekono base URL.
-            headers (List[str]): HTTP headers to send in key=value format.
-            body (str): HTTP body to send in JSON format.
-            no_verify (bool): Disable TLS validation.
-            show_headers (bool): Display HTTP response headers.
-            just_show_status_code (bool): Just display HTTP response status code.
-            quiet (bool): Don't display anything from response.
-            json_output (str): Filepath to the JSON file where content should be saved.
-        '''
-        client = RekonoApiCommand._rekono_factory(url, no_verify, headers)
-        response = client.post(RekonoApiCommand._get_endpoint(endpoint), RekonoApiCommand._get_body(body))
-        RekonoApiCommand._display_responses([response], show_headers, just_show_status_code, quiet)
-        RekonoApiCommand._save_output([response], json_output)
-
-    @staticmethod
-    @click.command(help='PUT request to Rekono API')
-    @endpoint_argument
-    @url_option
-    @headers_option
-    @body_option
-    @no_verify_option
-    @show_headers_option
-    @status_code_option
-    @quiet_option
-    @json_option
-    def put(
-        endpoint: str,
-        url: str,
-        headers: List[str],
-        body: str,
-        no_verify: bool,
-        show_headers: bool,
-        just_show_status_code: bool,
-        quiet: bool,
-        json_output: str
-    ):
-        '''PUT request to Rekono API.
-
-        Args:
-            endpoint (str): Endpoint to call.
-            url (str): Rekono base URL.
-            headers (List[str]): HTTP headers to send in key=value format.
-            body (str): HTTP body to send in JSON format.
-            no_verify (bool): Disable TLS validation.
-            show_headers (bool): Display HTTP response headers.
-            just_show_status_code (bool): Just display HTTP response status code.
-            quiet (bool): Don't display anything from response.
-            json_output (str): Filepath to the JSON file where content should be saved.
-        '''
-        client = RekonoApiCommand._rekono_factory(url, no_verify, headers)
-        response = client.put(RekonoApiCommand._get_endpoint(endpoint), RekonoApiCommand._get_body(body))
-        RekonoApiCommand._display_responses([response], show_headers, just_show_status_code, quiet)
-        RekonoApiCommand._save_output([response], json_output)
-
-    @staticmethod
-    @click.command(help='DELETE request to Rekono API')
-    @endpoint_argument
-    @url_option
-    @headers_option
-    @no_verify_option
-    @show_headers_option
-    @status_code_option
-    @quiet_option
-    def delete(
-        endpoint: str,
-        url: str,
-        headers: List[str],
-        no_verify: bool,
-        show_headers: bool,
-        just_show_status_code: bool,
-        quiet: bool
-    ):
-        '''DELETE request to Rekono API.
-
-        Args:
-            endpoint (str): Endpoint to call.
-            url (str): Rekono base URL.
-            headers (List[str]): HTTP headers to send in key=value format.
-            no_verify (bool): Disable TLS validation.
-            show_headers (bool): Display HTTP response headers.
-            just_show_status_code (bool): Just display HTTP response status code.
-            quiet (bool): Don't display anything from response.
-        '''
-        client = RekonoApiCommand._rekono_factory(url, no_verify, headers)
-        response = client.delete(RekonoApiCommand._get_endpoint(endpoint))
-        RekonoApiCommand._display_responses([response], show_headers, just_show_status_code, quiet)
diff --git a/src/rekono/framework/commands/__init__.py b/src/rekono/framework/commands/__init__.py
new file mode 100644
index 0000000..e726e01
--- /dev/null
+++ b/src/rekono/framework/commands/__init__.py
@@ -0,0 +1,1 @@
+'''Definition of base CLI commands extended by multiple commands.'''
diff --git a/src/rekono/framework/commands/api.py b/src/rekono/framework/commands/api.py
new file mode 100644
index 0000000..896e0b2
--- /dev/null
+++ b/src/rekono/framework/commands/api.py
@@ -0,0 +1,173 @@
+'''Base Rekono CLI command to make API requests.'''
+
+from typing import List
+
+import click
+
+from rekono.framework.arguments import endpoint_argument
+from rekono.framework.commands.command import RekonoCliCommand
+from rekono.framework.options import (all_pages_option, body_option,
+                                      file_option, headers_option, json_option,
+                                      no_verify_option, parameters_option,
+                                      quiet_option, show_headers_option,
+                                      show_status_code_option, url_option)
+
+
+class ApiCommand(RekonoCliCommand):
+    '''Base Rekono CLI command to make API requests.'''
+
+    commands = ['get', 'post', 'put', 'delete']                                 # List of supported commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get',
+        'post': 'post',
+        'put': 'put',
+        'delete': 'delete'
+    }
+    help_messages = {                                                           # Help messages for each command
+        'get': 'GET request to Rekono API',
+        'post': 'POST request to Rekono API',
+        'put': 'PUT request to Rekono API',
+        'delete': 'DELETE request to Rekono API'
+    }
+    api_options = [url_option, headers_option, no_verify_option]                # API options for all commands
+    display_options = [show_headers_option, show_status_code_option, quiet_option]  # Display options for all commands
+
+    @staticmethod
+    @click.command
+    @endpoint_argument
+    @parameters_option
+    @all_pages_option
+    @json_option
+    def get(
+        endpoint: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        parameters: List[str],
+        pagination: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ):
+        '''GET request to Rekono API.
+
+        Args:
+            endpoint (str): Endpoint to call.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            parameters (List[str]): HTTP query parameters to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            pagination (bool): Enable iteration over all API pages.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        client = ApiCommand._rekono_factory(url, no_verify, headers)
+        response_or_responses = client.get(
+            ApiCommand._get_endpoint(endpoint),
+            parameters=ApiCommand._parse_key_value_params(parameters),
+            pagination=pagination
+        )
+        responses = response_or_responses if isinstance(response_or_responses, list) else [response_or_responses]
+        ApiCommand._display_responses(responses, show_headers, only_show_status_code, quiet)
+        ApiCommand._save_output(responses, json_output)
+
+    @staticmethod
+    @click.command
+    @endpoint_argument
+    @body_option
+    @file_option
+    @json_option
+    def post(
+        endpoint: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        body: str,
+        filepath: str,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ):
+        '''POST request to Rekono API.
+
+        Args:
+            endpoint (str): Endpoint to call.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            body (str): HTTP body to send in JSON format.
+            filepath (click.Path): File to upload.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        client = ApiCommand._rekono_factory(url, no_verify, headers)
+        response = client.post(ApiCommand._get_endpoint(endpoint), ApiCommand._get_body(body), filepath)
+        ApiCommand._display_responses([response], show_headers, only_show_status_code, quiet)
+        ApiCommand._save_output([response], json_output)
+
+    @staticmethod
+    @click.command
+    @endpoint_argument
+    @body_option
+    @json_option
+    def put(
+        endpoint: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        body: str,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ):
+        '''PUT request to Rekono API.
+
+        Args:
+            endpoint (str): Endpoint to call.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            body (str): HTTP body to send in JSON format.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        client = ApiCommand._rekono_factory(url, no_verify, headers)
+        response = client.put(ApiCommand._get_endpoint(endpoint), ApiCommand._get_body(body))
+        ApiCommand._display_responses([response], show_headers, only_show_status_code, quiet)
+        ApiCommand._save_output([response], json_output)
+
+    @staticmethod
+    @click.command
+    @endpoint_argument
+    def delete(
+        endpoint: str,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ):
+        '''DELETE request to Rekono API.
+
+        Args:
+            endpoint (str): Endpoint to call.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        client = ApiCommand._rekono_factory(url, no_verify, headers)
+        response = client.delete(ApiCommand._get_endpoint(endpoint))
+        ApiCommand._display_responses([response], show_headers, only_show_status_code, quiet)
diff --git a/src/rekono/framework/commands/command.py b/src/rekono/framework/commands/command.py
new file mode 100644
index 0000000..32b77a1
--- /dev/null
+++ b/src/rekono/framework/commands/command.py
@@ -0,0 +1,233 @@
+'''Base features for Rekono CLI command.'''
+
+import json
+import os
+import sys
+from typing import Any, Callable, Dict, List, Optional, Union, cast
+from urllib.parse import urlparse
+
+import click
+import requests
+from requests.models import Response
+
+from rekono.client.api import Rekono
+
+
+class RekonoCliCommand(click.MultiCommand):
+    '''Base features for Rekono CLI command.'''
+
+    api_token_env = 'REKONO_TOKEN'                                              # Environment variable to set API token
+    commands: List[str] = []                                                    # List of supported commands
+    commands_mapping: Dict[str, str] = {}                                       # Mapping between commands and methods
+    default_mapping: Optional[str] = None                                       # Default method if mapping not found
+    help_messages: Dict[str, str] = {}                                          # Help messages for each command
+    api_options: List[Callable] = []                                            # API options for all commands
+    display_options: List[Callable] = []                                        # Display options for all commands
+    entity_options: List[Callable] = []                                         # Specific options for post and put
+
+    def list_commands(self, ctx: click.Context) -> List[str]:
+        '''Return list of CLI commands.
+
+        Args:
+            ctx (click.Context): Click context.
+
+        Returns:
+            List[str]: List of CLI commands.
+        '''
+        return self.commands
+
+    def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
+        '''Get CLI command by name.
+
+        Args:
+            ctx (click.Context): Click context.
+            cmd_name (str): Command name.
+
+        Returns:
+            Optional[click.Command]: Click command.
+        '''
+        related_command_method = self.commands_mapping.get(cmd_name, self.default_mapping)      # Get mapped method
+        if cmd_name in self.commands and related_command_method and hasattr(self, related_command_method):
+            command: click.Command = getattr(self, related_command_method)      # Get command method
+            command.help = self.help_messages.get(cmd_name)                     # Set help message
+            if related_command_method in ['post_entity', 'put_entity']:         # POST or PUT entity request
+                self._apply_command_options(command, self.entity_options)       # Set entity options
+            self._apply_command_options(command, self.api_options)              # Set base API options
+            self._apply_command_options(command, self.display_options)          # Set base display options
+            return command
+        return None
+
+    def _apply_command_options(self, command: Callable, options: List[Callable]) -> Callable:
+        '''Apply multiple options to specific command.
+
+        Args:
+            command (Callable): Command to configure
+            options (List[Callable]): List of options to apply
+
+        Returns:
+            Callable: Configured command
+        '''
+        for option in options:
+            command = option(command)
+        return command
+
+    @classmethod
+    def _get_url(cls, url: str) -> str:
+        '''Get valid Rekono base URL from the user provided value.
+
+        Args:
+            url (str): User provided URL.
+
+        Returns:
+            str: Valid Rekono URL.
+        '''
+        parser = urlparse(url)                                                  # Parse provided URL
+        if not parser.netloc:                                                   # Invalid URL
+            click.echo(click.style('URL is invalid', fg='red'), err=True, color=True)
+            url = click.prompt('URL', type=str)                                 # Ask user for base URL
+            return cls._get_url(url)                                            # Retry URL validation
+        return f'{parser.scheme or "https"}://{parser.netloc}'
+
+    @staticmethod
+    def _parse_key_value_params(items: List[str]) -> Dict[str, str]:
+        '''Parse key=value options.
+
+        Args:
+            items (List[str]): List of key=value values.
+
+        Returns:
+            Dict[str, str]: Dictionary with all keys and values.
+        '''
+        data = {}
+        for item in items or []:
+            if '=' in item:                                                     # Check separator
+                key, value = item.split('=', 1)                                 # Get key and value
+                data[key.strip()] = value.strip()
+        return data
+
+    @staticmethod
+    def _get_body(body: Optional[str]) -> Optional[str]:
+        '''Validate body value to be sent via HTTP.
+
+        Args:
+            body (Optional[str]): Body value.
+
+        Returns:
+            Optional[str]: Validated body value.
+        '''
+        if body:
+            try:
+                json.loads(body)                                                # Try to parse body value
+            except json.decoder.JSONDecodeError:                                # Invalid body value
+                click.echo(click.style('Invalid JSON format for body value', fg='red'), err=True, color=True)
+                sys.exit(1)
+        return body
+
+    @staticmethod
+    def _get_endpoint(endpoint: str) -> str:
+        '''Get valid endpoint value without parameters or other URL information.
+
+        Args:
+            endpoint (str): Endpoint value provided by user.
+
+        Returns:
+            str: Valid endpoint value.
+        '''
+        return urlparse(endpoint).path
+
+    @classmethod
+    def _rekono_factory(cls, url: str, no_verify: bool = False, headers: List[str] = []) -> Rekono:
+        '''Create Rekono client entity.
+
+        Args:
+            url (str): Base Rekono URL.
+            no_verify (bool, optional): Disable TLS validation. Defaults to False.
+            headers (List[str], optional): Extra HTTP request headers. Defaults to [].
+
+        Returns:
+            Rekono: Rekono API client.
+        '''
+        token = os.getenv(cls.api_token_env)                                    # Get API token from environment
+        if not token:                                                           # API token is not provided
+            token = click.prompt('API token', type=str, hide_input=True)        # Ask for API token
+        return Rekono(                                                          # Create Rekono API client
+            cls._get_url(url),                                                  # Get valid Rekono URL
+            token=cast(str, token),
+            headers=cls._parse_key_value_params(headers),                       # Get HTTP headers
+            verify=not no_verify
+        )
+
+    @staticmethod
+    def _get_data_from_responses(responses: List[Response]) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
+        '''Get data from Rekono API responses.
+
+        Args:
+            responses (List[Response]): List of Rekono API responses.
+
+        Returns:
+            Union[List[Dict[str, Any]], Dict[str, Any]]: Data returned by Rekono.
+        '''
+        content = []
+        for response in responses:                                              # For each response
+            try:
+                body = response.json()                                          # Parse JSON body from response
+            except requests.exceptions.JSONDecodeError:
+                continue
+            if isinstance(body, dict):                                          # Response body is a dictionary
+                body = body.get('results', body)                                # Get results field if it exists
+            if isinstance(body, list):                                          # Content is a list
+                content.extend(body)                                            # Save content
+            elif isinstance(body, dict):                                        # Content is a dictionary
+                if len(responses) == 1:                                         # Only one response
+                    return body                                                 # Return response content
+                content.append(body)                                            # Save content
+        return content
+
+    @classmethod
+    def _display_responses(
+        cls,
+        responses: List[Response],
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ) -> None:
+        '''Display Rekono API responses via standard output.
+
+        Args:
+            responses (List[Response]): Rekono API responses.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Only display status code from HTTP response.
+            quiet (bool): Don't display anything from response.
+        '''
+        if quiet:                                                               # No content should be displayed
+            return
+        if only_show_status_code or show_headers:                               # Headers or status should be displayed
+            for response in responses:                                          # For each response
+                if only_show_status_code:                                       # Just display status code
+                    click.echo(response.status_code)
+                elif show_headers:                                              # Show response headers
+                    click.echo()
+                    # Display HTTP request and response summary
+                    click.echo(f'{response.request.method} {response.request.path_url} {response.status_code}')
+                    for header, value in response.headers.items():
+                        click.echo(f'{header}: {value}')                        # Display HTTP response headers
+                    click.echo()
+                    data = cls._get_data_from_responses([response])             # Get content from response
+                    click.echo(json.dumps(data, ensure_ascii=True, indent=4))   # Display content via standard output
+        else:                                                                   # Standard display options
+            data = cls._get_data_from_responses(responses)                      # Get content from responses
+            click.echo(json.dumps(data, ensure_ascii=True, indent=4))           # Display content via standard output
+
+    @classmethod
+    def _save_output(cls, responses: List[Response], filepath: Optional[str]) -> None:
+        '''Save responses content in JSON file.
+
+        Args:
+            responses (List[Response]): Rekono API responses.
+            filepath (Optional[str]): Filepath to the JSON file where content should be saved.
+        '''
+        if not filepath:                                                        # JSON filepath isn't provided
+            return
+        data = cls._get_data_from_responses(responses)                          # Get data from responses
+        with open(filepath, 'w', encoding='utf-8') as file:                     # Open JSON file
+            json.dump(data, file, ensure_ascii=True, indent=4)                  # Write content in JSON file
diff --git a/src/rekono/framework/commands/entity.py b/src/rekono/framework/commands/entity.py
new file mode 100644
index 0000000..7fefdbd
--- /dev/null
+++ b/src/rekono/framework/commands/entity.py
@@ -0,0 +1,210 @@
+'''Base Rekono CLI command to make specific entity operations using API.'''
+
+import json
+from typing import Any, List, Optional, cast
+
+import click
+
+from rekono.framework.arguments import (id_mandatory_argument,
+                                        id_optional_argument)
+from rekono.framework.commands.api import ApiCommand
+from rekono.framework.options import json_option
+
+
+class EntityCommand(ApiCommand):
+    '''Base Rekono CLI command to make specific entity operations using API.'''
+
+    commands = ['get', 'create', 'update', 'delete']                            # CLI commands
+    commands_mapping = {                                                        # Mapping between commands and methods
+        'get': 'get_entity',
+        'create': 'post_entity',
+        'update': 'put_entity',
+        'delete': 'delete_entity',
+    }
+    default_mapping = 'extra_post_entity'                                       # Default method if mapping not found
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_optional_argument
+    @json_option
+    def get_entity(
+        ctx: click.Context,
+        id: Optional[int],
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str
+    ):
+        '''GET request to retrieve specific entities via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (Optional[int]): Entity Id to retrieve.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+        '''
+        parameters = {
+            'endpoint': f'/api/{cast(click.Context, ctx.parent).info_name}/',
+            'url': url,
+            'headers': headers,
+            'no_verify': no_verify,
+            'parameters': [],
+            'pagination': True,
+            'show_headers': show_headers,
+            'only_show_status_code': only_show_status_code,
+            'quiet': quiet,
+            'json_output': json_output
+        }
+        if id:
+            parameters.update({
+                'endpoint': f'/api/{cast(click.Context, ctx.parent).info_name}/{id}/',
+                'pagination': False
+            })
+        ctx.invoke(EntityCommand.get, **parameters)
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @json_option
+    def post_entity(
+        ctx: click.Context,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str,
+        **kwargs: Any
+    ) -> None:
+        '''POST request to create specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+            kwargs (Any): Variable fields that will be sent as body
+        '''
+        ctx.invoke(
+            EntityCommand.post, endpoint=f'/api/{cast(click.Context, ctx.parent).info_name}/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps(kwargs),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet,
+            json_output=json_output
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    @json_option
+    def put_entity(
+        ctx: click.Context,
+        id: int,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool,
+        json_output: str,
+        **kwargs: Any
+    ) -> None:
+        '''PUT request to update specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): Entity Id to updae.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+            json_output (str): Filepath to the JSON file where content should be saved.
+            kwargs (Any): Variable fields that will be sent as body.
+        '''
+        ctx.invoke(
+            EntityCommand.put, endpoint=f'/api/{cast(click.Context, ctx.parent).info_name}/{id}/',
+            url=url, headers=headers, no_verify=no_verify, body=json.dumps(kwargs),
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet,
+            json_output=json_output
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    def delete_entity(
+        ctx: click.Context,
+        id: int,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ):
+        '''DELETE request to remove specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): Entity Id to remove.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        ctx.invoke(
+            EntityCommand.delete, endpoint=f'/api/{cast(click.Context, ctx.parent).info_name}/{id}/',
+            url=url, headers=headers, no_verify=no_verify,
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet
+        )
+
+    @staticmethod
+    @click.command
+    @click.pass_context
+    @id_mandatory_argument
+    def extra_post_entity(
+        ctx: click.Context,
+        id: int,
+        url: str,
+        headers: List[str],
+        no_verify: bool,
+        show_headers: bool,
+        only_show_status_code: bool,
+        quiet: bool
+    ) -> None:
+        '''POST request to make some operation against specific entity via Rekono API.
+
+        Args:
+            ctx (click.Context): Click context.
+            id (int): Entity Id to updae.
+            url (str): Rekono base URL.
+            headers (List[str]): HTTP headers to send in key=value format.
+            no_verify (bool): Disable TLS validation.
+            show_headers (bool): Display HTTP response headers.
+            only_show_status_code (bool): Just display HTTP response status code.
+            quiet (bool): Don't display anything from response.
+        '''
+        ctx.invoke(
+            EntityCommand.post, endpoint=f'/api/{cast(click.Context, ctx.parent).info_name}/{id}/{ctx.info_name}/',
+            url=url, headers=headers, no_verify=no_verify, body=None,
+            show_headers=show_headers, only_show_status_code=only_show_status_code, quiet=quiet,
+            json_output=None
+        )
diff --git a/src/rekono/framework/options.py b/src/rekono/framework/options.py
index 8e04e7b..ecd1bdf 100644
--- a/src/rekono/framework/options.py
+++ b/src/rekono/framework/options.py
@@ -1,4 +1,4 @@
-'''Definition of CLI options.'''
+'''Definition of base CLI options used by multiple commands.'''
 
 import click
 
@@ -9,14 +9,20 @@
     help='Base URL to the Rekono backend'
 )
 
-headers_option = click.option(                                                  # Extra request header option
+headers_option = click.option(                                                  # Extra request headers option
     '-h', '--header', 'headers',
     multiple=True, type=str,
     required=False, default=[],
     help='HTTP header to send in format "<key>=value"'
 )
 
-parameters_option = click.option(                                               # Request parameter option
+no_verify_option = click.option(                                                # Option to disable TLS verification
+    '--no-verify', 'no_verify',
+    is_flag=True, default=False,
+    help='Disable TLS verification'
+)
+
+parameters_option = click.option(                                               # Request parameters option
     '-p', '--parameter', 'parameters',
     multiple=True, type=str,
     required=False, default=[],
@@ -25,36 +31,37 @@
 
 body_option = click.option(                                                     # Request body option
     '-b', '--body', 'body',
-    multiple=False, type=str, required=False,
-    help='HTTP body to send in JSON format'
+    type=str, required=False,
+    help='HTTP body to send in JSON'
 )
 
-no_verify_option = click.option(                                                # Option to disable TLS verification
-    '-n', '--no-verify', 'no_verify',
-    is_flag=True, default=False,
-    help='Disable TLS verification'
+file_option = click.option(                                                     # Filepath to upload option
+    '-f', '--file', 'filepath',
+    type=click.Path(exists=True),
+    required=False, default=None,
+    help='File to upload'
 )
 
 all_pages_option = click.option(                                                # Option to iterate over all API pages
-    '-a', '--all-pages', 'all_pages',
+    '-a', '--all-pages', 'pagination',
     is_flag=True, default=False,
     help='Perform pagination over all pages'
 )
 
 show_headers_option = click.option(                                             # Option to show response headers
-    '-S', '--show-headers', 'show_headers',
+    '-s', '--show-headers', 'show_headers',
     is_flag=True, default=False,
     help='Show response headers'
 )
 
-status_code_option = click.option(                                              # Option to only show response status
-    '-s', '--status-code', 'just_show_status_code',
+show_status_code_option = click.option(                                         # Option to only show response status
+    '--status-code', 'only_show_status_code',
     is_flag=True, default=False,
-    help='Just show response status code'
+    help='Only show response status code'
 )
 
 quiet_option = click.option(                                                    # Option to don't show anything
-    '-q', '--quiet', 'quiet',
+    '--quiet', 'quiet',
     is_flag=True, default=False,
     help='Don\'t show anything from response'
 )
@@ -64,3 +71,10 @@
     type=str, required=False,
     help='Save response data in JSON file'
 )
+
+tags_option = click.option(                                                     # Tags option used by multiple commands
+    '-t', '--tag', 'tags',
+    multiple=True, type=str,
+    required=False, default=[],
+    help='Related tags'
+)
diff --git a/src/rekono/main.py b/src/rekono/main.py
index 627352b..6371fd7 100644
--- a/src/rekono/main.py
+++ b/src/rekono/main.py
@@ -4,6 +4,23 @@
 
 from rekono import VERSION
 from rekono.commands.api import api
+from rekono.commands.authentications import authentications
+from rekono.commands.configurations import configurations
+from rekono.commands.executions import executions
+from rekono.commands.findings import (credentials, exploits, hosts, osint,
+                                      paths, ports, technologies,
+                                      vulnerabilities)
+from rekono.commands.processes import processes
+from rekono.commands.profile import profile
+from rekono.commands.projects import projects
+from rekono.commands.settings import settings
+from rekono.commands.steps import steps
+from rekono.commands.target_ports import target_ports
+from rekono.commands.targets import targets
+from rekono.commands.tasks import tasks
+from rekono.commands.tools import tools
+from rekono.commands.users import users
+from rekono.commands.wordlists import wordlists
 
 
 @click.group()
@@ -14,6 +31,28 @@ def rekono():
 
 # Add CLI commands
 rekono.add_command(api)
+rekono.add_command(authentications)
+rekono.add_command(configurations)
+rekono.add_command(credentials)
+rekono.add_command(executions)
+rekono.add_command(exploits)
+rekono.add_command(hosts)
+rekono.add_command(osint)
+rekono.add_command(paths)
+rekono.add_command(ports)
+rekono.add_command(processes)
+rekono.add_command(profile)
+rekono.add_command(projects)
+rekono.add_command(settings)
+rekono.add_command(steps)
+rekono.add_command(target_ports)
+rekono.add_command(targets)
+rekono.add_command(tasks)
+rekono.add_command(technologies)
+rekono.add_command(tools)
+rekono.add_command(users)
+rekono.add_command(vulnerabilities)
+rekono.add_command(wordlists)
 
 
 if __name__ == '__main__':


Test patch:
diff --git a/src/tests/framework.py b/src/tests/framework.py
index 657b4de..716b96e 100644
--- a/src/tests/framework.py
+++ b/src/tests/framework.py
@@ -3,26 +3,28 @@
 import json
 import os
 from typing import Any, Dict, List, Optional, Union
-from unittest import TestCase
+from unittest import TestCase, mock
 
 import click
 from click.testing import CliRunner
 
+from rekono.main import rekono
 from tests.mock import RekonoMock
 
 
 class RekonoCommandTest(TestCase):
     '''Framework for unit testing of Rekono CLI.'''
 
-    command: Optional[click.BaseCommand] = None                                 # CLI command to test
+    testing_filepath = 'test_json_export.json'                                  # Temporal file used for testing
+    unit_tests: List[Dict[str, Any]] = []                                       # List of unit tests to execute
 
-    def _test(
+    def _cli(
         self,
         arguments: List[str],
         output: Optional[str] = None,
         exit_code: int = 0,
-        json_file: Optional[str] = None,
-        invalid_url: bool = False
+        invalid_url: bool = False,
+        input_values: bool = True
     ) -> None:
         '''Test Rekono CLI command.
 
@@ -33,26 +35,41 @@ def _test(
             json_file (Optional[str], optional): JSON file where the output is saved. Defaults to None.
             invalid_url (bool, optional): Test command with invalid URL option. Defaults to False.
         '''
-        if not self.command:                                                    # Check CLI command
+        if not self.unit_tests:
             return
         runner = CliRunner()                                                    # Create CLI runner for testing
-        input_value = 'test\n'                                                  # Input value with API token
-        prefix = 'API token: \n'                                                # Prefix for expected output
-        if invalid_url:                                                         # Invalid URL is enabled
-            input_value += f'{RekonoMock.url}\n'                                # Add URL as input value
-            # Add invalid URL message to output
-            prefix += f'{click.style("URL is invalid", fg="red")}\nURL: {RekonoMock.url}\n'
-        result = runner.invoke(self.command, arguments, input=input_value)      # Invoke CLI command
-        terminal_output = prefix + (f'{output}\n' if output else '')            # Expected CLI command
-        self.assertEqual(exit_code, result.exit_code)
-        self.assertEqual(terminal_output, result.output)
-        if json_file:                                                           # If JSOn file provided
-            self.assertTrue(os.path.isfile(json_file))
-            with open(json_file, 'r', encoding='utf-8') as file:                # Open file to check its content
-                self.assertEqual(output, file.read())
-            os.remove(json_file)                                                # Remove testing file
+        input_value = None
+        prefix = ''
+        if input_values:
+            input_value = 'test\n'                                              # Input value with API token
+            prefix = 'API token: \n'                                            # Prefix for expected output
+            if invalid_url:                                                     # Invalid URL is enabled
+                input_value += f'{RekonoMock.url}\n'                            # Add URL as input value
+                # Add invalid URL message to output
+                prefix += f'{click.style("URL is invalid", fg="red")}\nURL: {RekonoMock.url}\n'
+        result = runner.invoke(rekono, arguments, input=input_value)            # Invoke CLI command
+        terminal_output = prefix + (f'{output}\n' if output else '')            # Expected output
+        self.assertEqual(exit_code, result.exit_code)                           # Check exit code
+        self.assertEqual(terminal_output, result.output)                        # Check terminal output
+        if os.path.isfile(self.testing_filepath):                               # If JSON file exists
+            with open(self.testing_filepath, 'r', encoding='utf-8') as file:    # Open file to check its content
+                self.assertEqual(output, file.read())                           # Check file content
+            os.remove(self.testing_filepath)                                    # Remove temporal testing file
 
-    def _json_body(self, content: Union[Dict[str, Any], List[Dict[str, Any]]]) -> str:
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_rekono_cli(self) -> None:
+        '''Execute configured unit tests.'''
+        for test in self.unit_tests or []:
+            self._cli(
+                test.get('arguments', []),
+                test.get('output'),
+                test.get('exit_code', 0),
+                test.get('invalid_url', False),
+                test.get('input_values', True)
+            )
+
+    @classmethod
+    def _json_body(cls, content: Union[Dict[str, Any], List[Dict[str, Any]]]) -> str:
         '''Create JSON body from content.
 
         Args:
@@ -63,8 +80,9 @@ def _json_body(self, content: Union[Dict[str, Any], List[Dict[str, Any]]]) -> st
         '''
         return json.dumps(content, ensure_ascii=True, indent=4)
 
+    @classmethod
     def _expected_output_with_headers(
-        self,
+        cls,
         method: str,
         status_code: int,
         response_headers: Dict[str, Any],
@@ -86,5 +104,5 @@ def _expected_output_with_headers(
         for key, value in response_headers.items():
             expected_output_with_headers += f'{key}: {value}\n'                 # Add header values
         expected_output_with_headers += '\n'
-        expected_output_with_headers += self._json_body(expected_output)        # Add expected body
+        expected_output_with_headers += cls._json_body(expected_output)         # Add expected body
         return expected_output_with_headers
diff --git a/src/tests/mock.py b/src/tests/mock.py
index b501f40..db848ce 100644
--- a/src/tests/mock.py
+++ b/src/tests/mock.py
@@ -39,20 +39,22 @@ def _response_factory(
         Returns:
             Response: HTTP response.
         '''
-        response = Response()
+        response = Response()                                                   # Build HTTP response
         response.request = Request(method=method, url=self.url + '/api/entities/').prepare()    # Set related request
-        response.status_code = status_code
-        response.headers = CaseInsensitiveDict(self.headers)
+        response.status_code = status_code                                      # Set response status code
+        response.headers = CaseInsensitiveDict(self.headers)                    # Set response headers
         response._content = json.dumps(content, ensure_ascii=True, indent=4).encode() if content else None  # Set body
         return response
 
-    def get(self, *args: Any, **kwargs: Any) -> Response:
+    def get(self, *args: Any, **kwargs: Any) -> Union[Response, List[Response]]:
         '''Mock GET request to Rekono API.
 
         Returns:
             Response: HTTP response.
         '''
-        return self._response_factory('GET', 200, self.data)
+        if kwargs.get('pagination', False):
+            return self.get_paginated_entities(*args, **kwargs)                 # Return paginated mock value
+        return self._response_factory('GET', 200, self.data)                    # Return standard response
 
     def get_multiple_entities(self, *args: Any, **kwargs: Any) -> Response:
         '''Mock GET request to Rekono API with multiple items.
@@ -82,6 +84,14 @@ def post(self, *args: Any, **kwargs: Any) -> Response:
         '''
         return self._response_factory('POST', 201, self.data)
 
+    def post_empty_response(self, *args: Any, **kwargs: Any) -> Response:
+        '''Mock POST request to Rekono API with empty response body.
+
+        Returns:
+            Response: HTTP response.
+        '''
+        return self._response_factory('POST', 201)
+
     def put(self, *args: Any, **kwargs: Any) -> Response:
         '''Mock PUT request to Rekono API.
 
diff --git a/src/tests/test_api.py b/src/tests/test_api.py
index 2ad0374..71623a6 100644
--- a/src/tests/test_api.py
+++ b/src/tests/test_api.py
@@ -1,11 +1,10 @@
 '''Test "api" CLI command.'''
 
 import json
-from unittest.mock import patch
+from unittest import mock
 
 import click
 
-from rekono.commands.api import api
 from tests.framework import RekonoCommandTest
 from tests.mock import RekonoMock
 
@@ -13,81 +12,73 @@
 class ApiTest(RekonoCommandTest):
     '''Test "api" CLI command.'''
 
-    command = api                                                               # Set CLI command
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['api', 'get', '-u', RekonoMock.url, '-p', 'key1=value1', '--no-verify', 'entities/1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['api', 'get', '--all-pages', '--no-verify', 'entities'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        },
+        {
+            'arguments': ['api', 'get', '-u', 'invalidurl', 'entities/1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data),
+            'invalid_url': True
+        },
+        {
+            'arguments': ['api', 'get', '--show-headers', 'entities/1'],
+            'output': RekonoCommandTest._expected_output_with_headers('GET', 200, RekonoMock.headers, RekonoMock.data)
+        },
+        {
+            'arguments': ['api', 'get', '--status-code', 'entities/1'],
+            'output': '200'
+        },
+        {
+            'arguments': ['api', 'get', '--quiet', 'entities/1']
+        },
+        {
+            'arguments': ['api', 'get', '--json', RekonoCommandTest.testing_filepath, 'entities/1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': [
+                'api', 'post', '-b', json.dumps(RekonoMock.data), '-h', 'key1=value1', '--no-verify', 'entities'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['api', 'post', '-b', 'invalid JSON value', 'entities'],
+            'output': click.style('Invalid JSON format for body value', fg='red'),
+            'exit_code': 1
+        },
+        {
+            'arguments': [
+                'api', 'put', '-b', json.dumps(RekonoMock.data), '-h', 'key1=value1', '--no-verify', 'entities/1'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['api', 'delete', '-h', 'key1=value1', '-h', 'key2=value2', '--no-verify', 'projects/1'],
+            'output': RekonoCommandTest._json_body([])
+        },
+        {
+            'arguments': ['api', 'notfound'],
+            'output': (
+                'Usage: rekono api [OPTIONS] COMMAND [ARGS]...\n'
+                'Try \'rekono api --help\' for help.\n\n'
+                'Error: No such command \'notfound\'.'
+            ),
+            'exit_code': 2,
+            'input_values': False
+        }
+    ]
 
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_get(self) -> None:
-        '''Test "api get" command.'''
-        self._test(
-            ['get', '-u', RekonoMock.url, '-p', 'key1=value1', '-p', 'key2=value2', '-n', 'entities/1'],
-            output=self._json_body(RekonoMock.data)
-        )
-        # Test command with invalid URL option
-        self._test(['get', '-u', 'invalidurl', 'entities/1'], output=self._json_body(RekonoMock.data), invalid_url=True)
-
-    @patch('rekono.framework.commands.Rekono.get', RekonoMock.get_multiple_entities)
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_get_multiple(self) -> None:
+    @mock.patch('rekono.framework.commands.command.Rekono.get', RekonoMock.get_multiple_entities)
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_get_multiple_entities(self) -> None:
         '''Test "api get" command with multiple entities as response.'''
-        self._test(
-            ['get', '-h', 'key1=value1', '-h', 'key2=value2', '--no-verify', 'entities'],
-            output=self._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
-        )
-
-    @patch('rekono.framework.commands.Rekono.get', RekonoMock.get_paginated_entities)
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_get_all_pages(self) -> None:
-        '''Test "api get" command with paginated responses.'''
-        self._test(
-            ['get', '--all-pages', '--no-verify', 'entities'],
-            output=self._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
-        )
-
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_post(self) -> None:
-        '''Test "api post" command.'''
-        self._test(
-            ['post', '-b', json.dumps(RekonoMock.data), '-h', 'key1=value1', '-h', 'key2=value2', '-n', 'entities'],
-            output=self._json_body(RekonoMock.data)
-        )
-        self._test(                                                             # Test command with invalid body option
-            ['post', '-b', 'invalid JSON value', 'entities'],
-            output=click.style('Invalid JSON format for body value', fg='red'),
-            exit_code=1
-        )
-
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_put(self) -> None:
-        '''Test "api put" command.'''
-        self._test(
-            ['put', '-b', json.dumps(RekonoMock.data), '-h', 'key1=value1', '-h', 'key2=value2', '-n', 'entities/1'],
-            output=self._json_body(RekonoMock.data)
-        )
-
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_delete(self) -> None:
-        '''Test "api delete" command.'''
-        self._test(
-            ['delete', '-h', 'key1=value1', '-h', 'key2=value2', '--no-verify', 'projects/1'],
-            output=self._json_body([])
-        )
-
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_display_options(self) -> None:
-        '''Test display options with "api get" command.'''
-        self._test(                                                             # Test show headers option
-            ['get', '--show-headers', 'entities/1'],
-            output=self._expected_output_with_headers('GET', 200, RekonoMock.headers, RekonoMock.data)
-        )
-        self._test(['get', '--status-code', 'entities/1'], output='200')        # Test status code option
-        self._test(['get', '--quiet', 'entities/1'])                            # Test quiet option
-
-    @patch('rekono.framework.commands.Rekono', RekonoMock)
-    def test_json_output(self) -> None:
-        '''Test JSON option with "api get" command.'''
-        test_filepath = 'test_json_export.json'
-        self._test(
-            ['get', '--json', test_filepath, 'entities/1'],
-            output=self._json_body(RekonoMock.data),
-            json_file=test_filepath
+        self._cli(
+            ['api', 'get', '-h', 'key1=value1', '-h', 'key2=value2', '--no-verify', 'entities'],
+            self._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
         )
diff --git a/src/tests/test_configurations.py b/src/tests/test_configurations.py
new file mode 100644
index 0000000..590f24c
--- /dev/null
+++ b/src/tests/test_configurations.py
@@ -0,0 +1,19 @@
+'''Test "configurations" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class ConfigurationsTest(RekonoCommandTest):
+    '''Test "configurations" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['configurations', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['configurations', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        }
+    ]
diff --git a/src/tests/test_executions.py b/src/tests/test_executions.py
new file mode 100644
index 0000000..5d0e6f3
--- /dev/null
+++ b/src/tests/test_executions.py
@@ -0,0 +1,19 @@
+'''Test "executions" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class ExecutionsTest(RekonoCommandTest):
+    '''Test "executions" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['executions', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['executions', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        }
+    ]
diff --git a/src/tests/test_findings.py b/src/tests/test_findings.py
new file mode 100644
index 0000000..07ae220
--- /dev/null
+++ b/src/tests/test_findings.py
@@ -0,0 +1,71 @@
+'''Test findings CLI command.'''
+
+from unittest import mock
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class FindingsTest(RekonoCommandTest):
+    '''Test findings CLI command.'''
+
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_get_one_finding(self) -> None:
+        '''Test to get one finding by ID.'''
+        self._cli([self.__class__.__name__.lower().replace('test', ''), 'get', '1'], self._json_body(RekonoMock.data))
+
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_get_multiple_findings(self) -> None:
+        '''Test to get multiple findings.'''
+        self._cli(
+            [self.__class__.__name__.lower().replace('test', ''), 'get'],
+            self._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        )
+
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_disable_finding(self) -> None:
+        '''Test to disable one finding.'''
+        self._cli([self.__class__.__name__.lower().replace('test', ''), 'disable', '1'], self._json_body([]))
+
+    @mock.patch('rekono.framework.commands.command.Rekono.post', RekonoMock.post_empty_response)
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_enable_finding(self) -> None:
+        '''Test to enable one finding.'''
+        self._cli([self.__class__.__name__.lower().replace('test', ''), 'enable', '1'], self._json_body([]))
+
+
+class CredentialsTest(FindingsTest):
+    '''Test "credentials" CLI command.'''
+
+
+class ExploitsTest(FindingsTest):
+    '''Test "exploits" CLI command.'''
+
+
+class HostsTest(FindingsTest):
+    '''Test "hosts" CLI command.'''
+
+
+class PortsTest(FindingsTest):
+    '''Test "ports" CLI command.'''
+
+
+class PathsTest(FindingsTest):
+    '''Test "paths" CLI command.'''
+
+
+class TechnologiesTest(FindingsTest):
+    '''Test "technologoies" CLI command.'''
+
+
+class VulnerabilitiesTest(FindingsTest):
+    '''Test "vulnerabilities" CLI command.'''
+
+
+class OSINTTest(FindingsTest):
+    '''Test "osint" CLI command.'''
+
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_target_creation(self) -> None:
+        '''Test to create one target from OSINT data.'''
+        self._cli(['osint', 'target', '1'], self._json_body(RekonoMock.data))
diff --git a/src/tests/test_profile.py b/src/tests/test_profile.py
new file mode 100644
index 0000000..32329df
--- /dev/null
+++ b/src/tests/test_profile.py
@@ -0,0 +1,19 @@
+'''Test "profile" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class ProfileTest(RekonoCommandTest):
+    '''Test "profile" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['profile', 'get'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['profile', 'telegram', '--token', 'telegramotp'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        }
+    ]
diff --git a/src/tests/test_projects.py b/src/tests/test_projects.py
new file mode 100644
index 0000000..e505bde
--- /dev/null
+++ b/src/tests/test_projects.py
@@ -0,0 +1,43 @@
+'''Test "projects" CLI command.'''
+
+from unittest import mock
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class ProjectsTest(RekonoCommandTest):
+    '''Test "projects" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['projects', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['projects', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        },
+        {
+            'arguments': ['projects', 'create', '--name', 'Process', '--description', 'Test', '--tag', 'test'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['projects', 'update', '1', '--name', 'Process', '--description', 'Test', '--tag', 'test'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['projects', 'delete', '1'],
+            'output': RekonoCommandTest._json_body([])
+        },
+        {
+            'arguments': ['projects', 'remove-member', '1', '--user', '1'],
+            'output': RekonoCommandTest._json_body([])
+        }
+    ]
+
+    @mock.patch('rekono.framework.commands.command.Rekono.post', RekonoMock.post_empty_response)
+    @mock.patch('rekono.framework.commands.command.Rekono', RekonoMock)
+    def test_add_member(self) -> None:
+        '''Test to add a new project member.'''
+        self._cli(['projects', 'add-member', '1', '--user', '1'], self._json_body([]))
diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py
new file mode 100644
index 0000000..2c8f5ae
--- /dev/null
+++ b/src/tests/test_settings.py
@@ -0,0 +1,15 @@
+'''Test "settings" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class SettingsTest(RekonoCommandTest):
+    '''Test "settings" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['settings', 'get'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        }
+    ]
diff --git a/src/tests/test_tasks.py b/src/tests/test_tasks.py
new file mode 100644
index 0000000..9e8718b
--- /dev/null
+++ b/src/tests/test_tasks.py
@@ -0,0 +1,58 @@
+'''Test "tasks" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class TasksTest(RekonoCommandTest):
+    '''Test "tasks" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['tasks', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['tasks', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        },
+        {
+            'arguments': ['tasks', 'create', '--target', '1', '--process', '1', '--intensity', 'Normal'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': [
+                'tasks', 'create', '--target', '1', '--tool', '1', '--configuration', '1', '--intensity', 'Hard'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': [
+                'tasks', 'create', '--target', '1', '--process', '1', '--intensity', 'Normal',
+                '--scheduled-at', '2000-01-01 01:00:00', '--wordlist', '1'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': [
+                'tasks', 'create', '--target', '1', '--process', '1', '--intensity', 'Normal',
+                '--scheduled-in', '5', '--scheduled-time-unit', 'Hours'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': [
+                'tasks', 'create', '--target', '1', '--process', '1', '--intensity', 'Normal',
+                '--repeat-in', '5', '--repeat-time-unit', 'Days'
+            ],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['tasks', 'cancel', '1'],
+            'output': RekonoCommandTest._json_body([])
+        },
+        {
+            'arguments': ['tasks', 'repeat', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        }
+    ]
diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py
new file mode 100644
index 0000000..2e9feeb
--- /dev/null
+++ b/src/tests/test_tools.py
@@ -0,0 +1,19 @@
+'''Test "tools" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class ToolsTest(RekonoCommandTest):
+    '''Test "tools" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['tools', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['tools', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        }
+    ]
diff --git a/src/tests/test_users.py b/src/tests/test_users.py
new file mode 100644
index 0000000..980e167
--- /dev/null
+++ b/src/tests/test_users.py
@@ -0,0 +1,31 @@
+'''Test "users" CLI command.'''
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class UsersTest(RekonoCommandTest):
+    '''Test "users" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['users', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['users', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        },
+        {
+            'arguments': ['users', 'delete', '1'],
+            'output': RekonoCommandTest._json_body([])
+        },
+        {
+            'arguments': ['users', 'role', '1', '--role', 'Reader'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['users', 'invite', '--email', 'newuser@rekono.com', '--role', 'Reader'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        }
+    ]
diff --git a/src/tests/test_wordlists.py b/src/tests/test_wordlists.py
new file mode 100644
index 0000000..ae64d6a
--- /dev/null
+++ b/src/tests/test_wordlists.py
@@ -0,0 +1,29 @@
+'''Test "wordlists" CLI command.'''
+
+import os
+
+from tests.framework import RekonoCommandTest
+from tests.mock import RekonoMock
+
+
+class WordlistsTest(RekonoCommandTest):
+    '''Test "wordlists" CLI command.'''
+
+    unit_tests = [                                                              # List of unit tests to execute
+        {
+            'arguments': ['wordlists', 'get', '1'],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['wordlists', 'get'],
+            'output': RekonoCommandTest._json_body([RekonoMock.data, RekonoMock.data, RekonoMock.data])
+        },
+        {
+            'arguments': ['wordlists', 'create', '-n', 'Wordlist', '-t', 'Endpoint', '-f', os.path.realpath(__file__)],
+            'output': RekonoCommandTest._json_body(RekonoMock.data)
+        },
+        {
+            'arguments': ['wordlists', 'delete', '1'],
+            'output': RekonoCommandTest._json_body([])
+        }
+    ]