From 860fdb38107b3210c53190bdc4c53b2c56947ee2 Mon Sep 17 00:00:00 2001 From: i_virus Date: Wed, 21 Aug 2024 19:06:15 -0400 Subject: [PATCH] Fixes: #1298 - Add SnipeIT source (#1299) ### Summary Adding new intel module for SnipeIT source. ### Related issues #1298 ___ Read through our [developer docs](https://lyft.github.io/cartography/dev/developer-guide.html) - [x] PR Title starts with "Fixes: [issue number]" If you are modifying or implementing a new intel module - [x] Update the [schema](https://github.com/lyft/cartography/tree/master/docs/root/modules) and [readme](https://github.com/lyft/cartography/blob/master/docs/schema/README.md) - [x] Use our NodeSchema [data model](https://lyft.github.io/cartography/dev/writing-intel-modules.html#defining-a-node) - [x] Use specialized functions `get_`, `transform_`, `load_`, and `cleanup_` functions - [x] Add [tests](https://lyft.github.io/cartography/dev/writing-intel-modules.html#making-tests) - [x] Unit tests: Test your `transform_` function with sample data (Not applicable) - Integration tests - [x] Use our test [helper functions](https://github.com/lyft/cartography/blob/master/tests/integration/util.py) - [x] Test a cleanup job --- README.md | 4 +- cartography/cli.py | 42 ++ cartography/config.py | 12 + cartography/intel/snipeit/__init__.py | 30 ++ cartography/intel/snipeit/asset.py | 74 ++++ cartography/intel/snipeit/user.py | 75 ++++ cartography/intel/snipeit/util.py | 35 ++ cartography/models/snipeit/__init__.py | 0 cartography/models/snipeit/asset.py | 81 ++++ cartography/models/snipeit/tenant.py | 17 + cartography/models/snipeit/user.py | 49 +++ cartography/sync.py | 2 + docs/root/modules/snipeit/config.md | 11 + docs/root/modules/snipeit/index.rst | 13 + docs/root/modules/snipeit/schema.md | 55 +++ tests/data/snipeit/__init__.py | 0 tests/data/snipeit/assets.py | 378 ++++++++++++++++++ tests/data/snipeit/tenants.py | 8 + tests/data/snipeit/users.py | 262 ++++++++++++ .../cartography/intel/snipeit/__init__.py | 0 .../intel/snipeit/test_snipeit_assets.py | 183 +++++++++ .../intel/snipeit/test_snipeit_users.py | 159 ++++++++ 22 files changed, 1489 insertions(+), 1 deletion(-) create mode 100644 cartography/intel/snipeit/__init__.py create mode 100644 cartography/intel/snipeit/asset.py create mode 100644 cartography/intel/snipeit/user.py create mode 100644 cartography/intel/snipeit/util.py create mode 100644 cartography/models/snipeit/__init__.py create mode 100644 cartography/models/snipeit/asset.py create mode 100644 cartography/models/snipeit/tenant.py create mode 100644 cartography/models/snipeit/user.py create mode 100644 docs/root/modules/snipeit/config.md create mode 100644 docs/root/modules/snipeit/index.rst create mode 100644 docs/root/modules/snipeit/schema.md create mode 100644 tests/data/snipeit/__init__.py create mode 100644 tests/data/snipeit/assets.py create mode 100644 tests/data/snipeit/tenants.py create mode 100644 tests/data/snipeit/users.py create mode 100644 tests/integration/cartography/intel/snipeit/__init__.py create mode 100644 tests/integration/cartography/intel/snipeit/test_snipeit_assets.py create mode 100644 tests/integration/cartography/intel/snipeit/test_snipeit_users.py diff --git a/README.md b/README.md index ad5242bb8..450dcc4d4 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Start [here](https://lyft.github.io/cartography/install.html). - [Lastpass](https://lyft.github.io/cartography/modules/lastpass/index.html) - users - [BigFix](https://lyft.github.io/cartography/modules/bigfix/index.html) - Computers - [Duo](https://lyft.github.io/cartography/modules/duo/index.html) - Users, Groups, Endpoints - +- [Kandji](https://lyft.github.io/cartography/modules/kandji/index.html) - Devices +- [SnipeIT](https://lyft.github.io/cartography/modules/snipeit/index.html) - Users, Assets ## Usage Start with our [tutorial](https://lyft.github.io/cartography/usage/tutorial.html). Our [data schema](https://lyft.github.io/cartography/usage/schema.html) is a helpful reference when you get stuck. @@ -74,6 +75,7 @@ and follow the instructions to sign the CLA. 1. [MessageBird](https://messagebird.com) 1. [Cloudanix](https://www.cloudanix.com/) 1. [ZeusCloud](https://www.zeuscloud.io/) +1. [Corelight](https://www.corelight.com/) 1. {Your company here} :-) If your organization uses Cartography, please file a PR and update this list. Say hi on Slack too! diff --git a/cartography/cli.py b/cartography/cli.py index 95c12a64d..b50e98374 100644 --- a/cartography/cli.py +++ b/cartography/cli.py @@ -541,6 +541,28 @@ def _build_parser(self): 'Required if you are using the Semgrep intel module. Ignored otherwise.' ), ) + parser.add_argument( + '--snipeit-base-uri', + type=str, + default=None, + help=( + 'Your SnipeIT base URI' + 'Required if you are using the SnipeIT intel module. Ignored otherwise.' + ), + ) + parser.add_argument( + '--snipeit-token-env-var', + type=str, + default=None, + help='The name of an environment variable containing token with which to authenticate to SnipeIT.', + ) + parser.add_argument( + '--snipeit-tenant-id', + type=str, + default=None, + help='An ID for the SnipeIT tenant.', + ) + return parser def main(self, argv: str) -> int: @@ -744,6 +766,26 @@ def main(self, argv: str) -> int: else: config.cve_api_key = None + # SnipeIT config + if config.snipeit_base_uri: + if config.snipeit_token_env_var: + logger.debug( + "Reading SnipeIT API token from environment variable '%s'.", + config.snipeit_token_env_var, + ) + config.snipeit_token = os.environ.get(config.snipeit_token_env_var) + elif os.environ.get('SNIPEIT_TOKEN'): + logger.debug( + "Reading SnipeIT API token from environment variable 'SNIPEIT_TOKEN'.", + ) + config.snipeit_token = os.environ.get('SNIPEIT_TOKEN') + else: + logger.warning("A SnipeIT base URI was provided but a token was not.") + config.kandji_token = None + else: + logger.warning("A SnipeIT base URI was not provided.") + config.snipeit_base_uri = None + # Run cartography try: return cartography.sync.run_with_config(self.sync, config) diff --git a/cartography/config.py b/cartography/config.py index 5ea9cee00..6dfe82fac 100644 --- a/cartography/config.py +++ b/cartography/config.py @@ -111,6 +111,12 @@ class Config: :param duo_api_hostname: The Duo api hostname, e.g. "api-abc123.duosecurity.com". Optional. :param semgrep_app_token: The Semgrep api token. Optional. :type semgrep_app_token: str + :type snipeit_base_uri: string + :param snipeit_base_uri: SnipeIT data provider base URI. Optional. + :type snipeit_token: string + :param snipeit_token: Token used to authenticate to the SnipeIT data provider. Optional. + :type snipeit_tenant_id: string + :param snipeit_tenant_id: Token used to authenticate to the SnipeIT data provider. Optional. """ def __init__( @@ -170,6 +176,9 @@ def __init__( duo_api_secret=None, duo_api_hostname=None, semgrep_app_token=None, + snipeit_base_uri=None, + snipeit_token=None, + snipeit_tenant_id=None, ): self.neo4j_uri = neo4j_uri self.neo4j_user = neo4j_user @@ -226,3 +235,6 @@ def __init__( self.duo_api_secret = duo_api_secret self.duo_api_hostname = duo_api_hostname self.semgrep_app_token = semgrep_app_token + self.snipeit_base_uri = snipeit_base_uri + self.snipeit_token = snipeit_token + self.snipeit_tenant_id = snipeit_tenant_id diff --git a/cartography/intel/snipeit/__init__.py b/cartography/intel/snipeit/__init__.py new file mode 100644 index 000000000..55d08d7a8 --- /dev/null +++ b/cartography/intel/snipeit/__init__.py @@ -0,0 +1,30 @@ +import logging + +import neo4j + +from cartography.config import Config +from cartography.intel.snipeit import asset +from cartography.intel.snipeit import user +from cartography.stats import get_stats_client +from cartography.util import timeit + +logger = logging.getLogger(__name__) +stat_handler = get_stats_client(__name__) + + +@timeit +def start_snipeit_ingestion(neo4j_session: neo4j.Session, config: Config) -> None: + if config.snipeit_base_uri is None or config.snipeit_token is None or config.snipeit_tenant_id is None: + logger.warning( + "Required parameter(s) missing. Skipping sync.", + ) + return + + common_job_parameters = { + "UPDATE_TAG": config.update_tag, + "TENANT_ID": config.snipeit_tenant_id, + } + + # Ingest SnipeIT users and assets + user.sync(neo4j_session, common_job_parameters, config.snipeit_base_uri, config.snipeit_token) + asset.sync(neo4j_session, common_job_parameters, config.snipeit_base_uri, config.snipeit_token) diff --git a/cartography/intel/snipeit/asset.py b/cartography/intel/snipeit/asset.py new file mode 100644 index 000000000..09a6d24d6 --- /dev/null +++ b/cartography/intel/snipeit/asset.py @@ -0,0 +1,74 @@ +import logging +from typing import Any +from typing import Dict +from typing import List + +import neo4j + +from .util import call_snipeit_api +from cartography.client.core.tx import load +from cartography.graph.job import GraphJob +from cartography.models.snipeit.asset import SnipeitAssetSchema +from cartography.models.snipeit.tenant import SnipeitTenantSchema +from cartography.util import timeit + + +logger = logging.getLogger(__name__) + + +@timeit +def get(base_uri: str, token: str) -> List[Dict]: + api_endpoint = "/api/v1/hardware" + results: List[Dict[str, Any]] = [] + while True: + offset = len(results) + api_endpoint = f"{api_endpoint}?order='asc'&offset={offset}" + response = call_snipeit_api(api_endpoint, base_uri, token) + results.extend(response['rows']) + + total = response['total'] + results_count = len(results) + if results_count >= total: + break + + return results + + +@timeit +def load_assets( + neo4j_session: neo4j.Session, + common_job_parameters: Dict, + data: List[Dict[str, Any]], +) -> None: + # Create the SnipeIT Tenant + load( + neo4j_session, + SnipeitTenantSchema(), + [{'id': common_job_parameters["TENANT_ID"]}], + lastupdated=common_job_parameters["UPDATE_TAG"], + ) + + load( + neo4j_session, + SnipeitAssetSchema(), + data, + lastupdated=common_job_parameters["UPDATE_TAG"], + TENANT_ID=common_job_parameters["TENANT_ID"], + ) + + +@timeit +def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None: + GraphJob.from_node_schema(SnipeitAssetSchema(), common_job_parameters).run(neo4j_session) + + +@timeit +def sync( + neo4j_session: neo4j.Session, + common_job_parameters: Dict, + base_uri: str, + token: str, +) -> None: + assets = get(base_uri=base_uri, token=token) + load_assets(neo4j_session=neo4j_session, common_job_parameters=common_job_parameters, data=assets) + cleanup(neo4j_session, common_job_parameters) diff --git a/cartography/intel/snipeit/user.py b/cartography/intel/snipeit/user.py new file mode 100644 index 000000000..9271ee044 --- /dev/null +++ b/cartography/intel/snipeit/user.py @@ -0,0 +1,75 @@ +import logging +from typing import Any +from typing import Dict +from typing import List + +import neo4j + +from .util import call_snipeit_api +from cartography.client.core.tx import load +from cartography.graph.job import GraphJob +from cartography.models.snipeit.tenant import SnipeitTenantSchema +from cartography.models.snipeit.user import SnipeitUserSchema +from cartography.util import timeit + +logger = logging.getLogger(__name__) + + +@timeit +def get(base_uri: str, token: str) -> List[Dict]: + api_endpoint = "/api/v1/users" + results: List[Dict[str, Any]] = [] + while True: + offset = len(results) + api_endpoint = f"{api_endpoint}?order='asc'&offset={offset}" + response = call_snipeit_api(api_endpoint, base_uri, token) + results.extend(response['rows']) + + total = response['total'] + results_count = len(results) + if results_count >= total: + break + + return results + + +@timeit +def load_users( + neo4j_session: neo4j.Session, + common_job_parameters: Dict, + data: List[Dict[str, Any]], +) -> None: + logger.debug(data[0]) + + # Create the SnipeIT Tenant + load( + neo4j_session, + SnipeitTenantSchema(), + [{'id': common_job_parameters["TENANT_ID"]}], + lastupdated=common_job_parameters["UPDATE_TAG"], + ) + + load( + neo4j_session, + SnipeitUserSchema(), + data, + lastupdated=common_job_parameters["UPDATE_TAG"], + TENANT_ID=common_job_parameters["TENANT_ID"], + ) + + +@timeit +def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None: + GraphJob.from_node_schema(SnipeitUserSchema(), common_job_parameters).run(neo4j_session) + + +@timeit +def sync( + neo4j_session: neo4j.Session, + common_job_parameters: Dict, + base_uri: str, + token: str, +) -> None: + users = get(base_uri=base_uri, token=token) + load_users(neo4j_session, common_job_parameters, users) + cleanup(neo4j_session, common_job_parameters) diff --git a/cartography/intel/snipeit/util.py b/cartography/intel/snipeit/util.py new file mode 100644 index 000000000..fbacee487 --- /dev/null +++ b/cartography/intel/snipeit/util.py @@ -0,0 +1,35 @@ +import logging +from typing import Any +from typing import Dict + +import requests + +from cartography.util import timeit + +logger = logging.getLogger(__name__) +# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts +_TIMEOUT = (60, 60) + + +@timeit +def call_snipeit_api(api_and_parameters: str, base_uri: str, token: str) -> Dict[str, Any]: + uri = base_uri + api_and_parameters + try: + logger.debug( + "SnipeIT: Get %s", uri, + ) + response = requests.get( + uri, + headers={ + 'Accept': 'application/json', + 'Authorization': f'Bearer {token}', + }, + timeout=_TIMEOUT, + ) + except requests.exceptions.Timeout: + # Add context and re-raise for callers to handle + logger.warning(f"SnipeIT: requests.get('{uri}') timed out.") + raise + # if call failed, use requests library to raise an exception + response.raise_for_status() + return response.json() diff --git a/cartography/models/snipeit/__init__.py b/cartography/models/snipeit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cartography/models/snipeit/asset.py b/cartography/models/snipeit/asset.py new file mode 100644 index 000000000..5af2dbfcd --- /dev/null +++ b/cartography/models/snipeit/asset.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass + +from cartography.models.core.common import PropertyRef +from cartography.models.core.nodes import CartographyNodeProperties +from cartography.models.core.nodes import CartographyNodeSchema +from cartography.models.core.relationships import CartographyRelProperties +from cartography.models.core.relationships import CartographyRelSchema +from cartography.models.core.relationships import LinkDirection +from cartography.models.core.relationships import make_target_node_matcher +from cartography.models.core.relationships import OtherRelationships +from cartography.models.core.relationships import TargetNodeMatcher + + +@dataclass(frozen=True) +class SnipeitAssetNodeProperties(CartographyNodeProperties): + """ + https://snipe-it.readme.io/reference/hardware-list + """ + # Common properties + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + # SnipeIT specific properties + asset_tag: PropertyRef = PropertyRef('asset_tag') + assigned_to: PropertyRef = PropertyRef('assigned_to.email') + category: PropertyRef = PropertyRef('category.name') + company: PropertyRef = PropertyRef('company.name') + manufacturer: PropertyRef = PropertyRef('manufacturer.name') + model: PropertyRef = PropertyRef('model.name') + serial: PropertyRef = PropertyRef('serial', extra_index=True) + + +### +# (:SnipeitAsset)<-[:ASSET]-(:SnipeitTenant) +### +@dataclass(frozen=True) +class SnipeitTenantToSnipeitAssetRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +class SnipeitTenantToSnipeitAssetRel(CartographyRelSchema): + target_node_label: str = 'SnipeitTenant' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('TENANT_ID', set_in_kwargs=True)}, + ) + direction: LinkDirection = LinkDirection.INWARD + rel_label: str = "HAS_ASSET" + properties: SnipeitTenantToSnipeitAssetRelProperties = SnipeitTenantToSnipeitAssetRelProperties() + + +### +# (:SnipeitUser)-[:HAS_CHECKED_OUT]->(:SnipeitAsset) +### +@dataclass(frozen=True) +class SnipeitUserToSnipeitAssetProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +class SnipeitUserToSnipeitAssetRel(CartographyRelSchema): + target_node_label: str = 'SnipeitUser' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'email': PropertyRef('assigned_to.email')}, + ) + direction: LinkDirection = LinkDirection.INWARD + rel_label: str = "HAS_CHECKED_OUT" + properties: SnipeitUserToSnipeitAssetProperties = SnipeitUserToSnipeitAssetProperties() + + +### +@dataclass(frozen=True) +class SnipeitAssetSchema(CartographyNodeSchema): + label: str = 'SnipeitAsset' # The label of the node + properties: SnipeitAssetNodeProperties = SnipeitAssetNodeProperties() # An object representing all properties + sub_resource_relationship: SnipeitTenantToSnipeitAssetRel = SnipeitTenantToSnipeitAssetRel() + other_relationships: OtherRelationships = OtherRelationships( + [ + SnipeitUserToSnipeitAssetRel(), + ], + ) diff --git a/cartography/models/snipeit/tenant.py b/cartography/models/snipeit/tenant.py new file mode 100644 index 000000000..9c4ca0a9e --- /dev/null +++ b/cartography/models/snipeit/tenant.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from cartography.models.core.common import PropertyRef +from cartography.models.core.nodes import CartographyNodeProperties +from cartography.models.core.nodes import CartographyNodeSchema + + +@dataclass(frozen=True) +class SnipeitTenantNodeProperties(CartographyNodeProperties): + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +class SnipeitTenantSchema(CartographyNodeSchema): + label: str = 'SnipeitTenant' # The label of the node + properties: SnipeitTenantNodeProperties = SnipeitTenantNodeProperties() # An object representing all properties diff --git a/cartography/models/snipeit/user.py b/cartography/models/snipeit/user.py new file mode 100644 index 000000000..a433d5685 --- /dev/null +++ b/cartography/models/snipeit/user.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass + +from cartography.models.core.common import PropertyRef +from cartography.models.core.nodes import CartographyNodeProperties +from cartography.models.core.nodes import CartographyNodeSchema +from cartography.models.core.relationships import CartographyRelProperties +from cartography.models.core.relationships import CartographyRelSchema +from cartography.models.core.relationships import LinkDirection +from cartography.models.core.relationships import make_target_node_matcher +from cartography.models.core.relationships import TargetNodeMatcher + + +@dataclass(frozen=True) +class SnipeitUserNodeProperties(CartographyNodeProperties): + """ + Ref: https://snipe-it.readme.io/reference/users + """ + # Common properties + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + # SnipeIT specific properties + company: PropertyRef = PropertyRef('company_id.name', extra_index=True) + email: PropertyRef = PropertyRef('email', extra_index=True) + username: PropertyRef = PropertyRef('username') + + +@dataclass(frozen=True) +class SnipeitTenantToSnipeitUserRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +# (:SnipeitTenant)-[:HAS_USER]->(:SnipeitUser) +class SnipeitTenantToSnipeitUserRel(CartographyRelSchema): + target_node_label: str = 'SnipeitTenant' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('TENANT_ID', set_in_kwargs=True)}, + ) + direction: LinkDirection = LinkDirection.INWARD + rel_label: str = "HAS_USER" + properties: SnipeitTenantToSnipeitUserRelProperties = SnipeitTenantToSnipeitUserRelProperties() + + +@dataclass(frozen=True) +class SnipeitUserSchema(CartographyNodeSchema): + label: str = 'SnipeitUser' # The label of the node + properties: SnipeitUserNodeProperties = SnipeitUserNodeProperties() # An object representing all properties + sub_resource_relationship: SnipeitTenantToSnipeitUserRel = SnipeitTenantToSnipeitUserRel() diff --git a/cartography/sync.py b/cartography/sync.py index 7117fe0dc..d4b14e47c 100644 --- a/cartography/sync.py +++ b/cartography/sync.py @@ -30,6 +30,7 @@ import cartography.intel.oci import cartography.intel.okta import cartography.intel.semgrep +import cartography.intel.snipeit from cartography.config import Config from cartography.stats import set_stats_client from cartography.util import STATUS_FAILURE @@ -57,6 +58,7 @@ 'bigfix': cartography.intel.bigfix.start_bigfix_ingestion, 'duo': cartography.intel.duo.start_duo_ingestion, 'semgrep': cartography.intel.semgrep.start_semgrep_ingestion, + 'snipeit': cartography.intel.snipeit.start_snipeit_ingestion, 'analysis': cartography.intel.analysis.run, }) diff --git a/docs/root/modules/snipeit/config.md b/docs/root/modules/snipeit/config.md new file mode 100644 index 000000000..67dd38ba7 --- /dev/null +++ b/docs/root/modules/snipeit/config.md @@ -0,0 +1,11 @@ +## SnipeIT Configuration + +.. _SnipeIT_config: + +Follow these steps to analyze SnipeIT users and assets in Cartography. + +1. Prepare an API token for SnipeIT + 1. Follow [SnipeIT documentation](https://snipe-it.readme.io/reference/generating-api-tokens) to generate a API token. + 1. Populate `SNIPEIT_TOKEN` environment variable with the API token. Alternately, you can pass a different environment variable name containing the API token + via CLI with the `--snipeit-token-env-var` parameter. + 1. Provide the SnipeIT API URL using the `--snipeit-base-uri` and a SnipeIT Tenant (required for establishing relationship) using the `--snipeit-tenant-id` parameter. diff --git a/docs/root/modules/snipeit/index.rst b/docs/root/modules/snipeit/index.rst new file mode 100644 index 000000000..fb6da1030 --- /dev/null +++ b/docs/root/modules/snipeit/index.rst @@ -0,0 +1,13 @@ +SnipeIT +####### + +The SnipeIT module has the following coverage: + +* Users +* Assets + +.. toctree:: + :hidden: + :glob: + + * diff --git a/docs/root/modules/snipeit/schema.md b/docs/root/modules/snipeit/schema.md new file mode 100644 index 000000000..2fae1722e --- /dev/null +++ b/docs/root/modules/snipeit/schema.md @@ -0,0 +1,55 @@ +## SnipeIT Schema + +.. _snipeit_schema: + +### SnipeitTenant + +Representation of a SnipeIT Tenant. + +|Field | Description| +|-------|-------------| +|id | SnipeIT Tenant ID e.g. "company name"| + +### SnipeitUser + +Representation of a SnipeIT User. + +|Field | Description| +|-------|-------------| +|id | same as device_id| +|company | Company the SnipeIT user is linked to| +|username | Username of the user | +|email | Email of the user | + +### SnipeitAsset + +Representation of a SnipeIT asset. + +|Field | Description| +|-------|-------------| +|id | Asset id| +|asset_tag | Asset tag| +|assigned_to | Email of the SnipeIT user the asset is checked out to| +|category | Category of the asset | +|company | The company the asset belongs to | +|manufacturer | Manufacturer of the asset | +|model | Model of the device| +|serial | Serial number of the asset| + +#### Relationships + +- All SnipeIT users and asset are linked to a SnipeIT Tenant + + ```cypher + (:SnipeitUser)<-[:HAS_USER]-(:SnipeitTenant) + ``` + + ```cypher + (:SnipeitAsset)<-[:HAS_ASSET]-(:SnipeitTenant) + ``` + +- A SnipeIT user can check-out one or more assets + + ```cypher + (:SnipeitAsset)<-[:HAS_CHECKED_OUT]-(:SnipeitUser) + ``` diff --git a/tests/data/snipeit/__init__.py b/tests/data/snipeit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/snipeit/assets.py b/tests/data/snipeit/assets.py new file mode 100644 index 000000000..4913f5b56 --- /dev/null +++ b/tests/data/snipeit/assets.py @@ -0,0 +1,378 @@ +ASSETS = { + "company_a": [ + { + "id": 1373, + "name": "", + "asset_tag": "86285367", + "serial": "C02ZJ48XXXXX", + "model": { + "id": 1, + "name": "Macbook Pro", + }, + "model_number": "2490747520276188", + "eol": { + "date": "2023-09-16", + "formatted": "Sat Sep 16, 2023", + }, + 'status_label': { + 'id': 2, + 'name': 'Ready to Deploy', + 'status_type': 'deployable', + 'status_meta': 'deployed', + }, + "category": { + "id": 2, + "name": "Laptop", + }, + "manufacturer": { + "id": 1, + "name": "Apple", + }, + "supplier": { + "id": 1, + "name": "Apple", + }, + "notes": "Created by DB seeder", + "order_number": "26993588", + "company": None, + "location": { + "id": 1, + "name": "Office", + }, + "rtd_location": { + "id": 1, + "name": "Office", + }, + "image": "https://develop.snipeitapp.com/uploads/models/ultrasharp.jpg", + "qr": "https://develop.snipeitapp.com/uploads/barcodes/qr-86285367-1373.png", + "alt_barcode": None, + "assigned_to": { + "id": 1, + "name": "Admin User", + "first_name": "Admin", + "last_name": "User", + "username": "admin", + "employee_num": "12832", + "email": "mcarter@example.net", + 'type': 'user', + }, + "warranty_months": None, + "warranty_expires": None, + "created_at": { + "datetime": "2022-12-19 15:42:29", + "formatted": "Mon Dec 19, 2022 3:42PM", + }, + "updated_at": { + "datetime": "2022-12-19 15:42:44", + "formatted": "Mon Dec 19, 2022 3:42PM", + }, + "last_audit_date": None, + "next_audit_date": None, + "deleted_at": None, + "purchase_date": { + "date": "2022-09-16", + "formatted": "Fri Sep 16, 2022", + }, + "age": "3m 3d", + "last_checkout": None, + "expected_checkin": None, + "purchase_cost": "446,80", + "checkin_counter": 0, + "checkout_counter": 0, + "requests_counter": 0, + "user_can_checkout": False, + "custom_fields": {}, + "available_actions": { + "checkout": True, + "checkin": True, + "clone": True, + "restore": True, + "update": True, + "delete": True, + }, + }, + { + "id": 1372, + "name": "", + "asset_tag": "429167203", + "serial": "72ec94a8-b6dc-37f1-b2a9-0907806e8db7", + "model": { + "id": 18, + "name": "Ultrasharp U2415", + }, + "model_number": "2490747520276188", + "eol": { + "date": "2023-10-31", + "formatted": "Tue Oct 31, 2023", + }, + "status_label": { + "id": 1, + "name": "Ready to Deploy", + "status_type": "deployable", + "status_meta": "deployable", + }, + "category": { + "id": 5, + "name": "Displays", + }, + "manufacturer": { + "id": 3, + "name": "Dell", + }, + "supplier": { + "id": 1, + "name": "Murphy, Prohaska and Hudson", + }, + "notes": "Created by DB seeder", + "order_number": "12818712", + "company": None, + "location": { + "id": 2, + "name": "East Betty", + }, + "rtd_location": { + "id": 2, + "name": "East Betty", + }, + "image": "https://develop.snipeitapp.com/uploads/models/ultrasharp.jpg", + "qr": "https://develop.snipeitapp.com/uploads/barcodes/qr-429167203-1372.png", + "alt_barcode": None, + "assigned_to": None, + "warranty_months": None, + "warranty_expires": None, + "created_at": { + "datetime": "2022-12-19 15:42:29", + "formatted": "Mon Dec 19, 2022 3:42PM", + }, + "updated_at": { + "datetime": "2022-12-19 15:42:44", + "formatted": "Mon Dec 19, 2022 3:42PM", + }, + "last_audit_date": None, + "next_audit_date": None, + "deleted_at": None, + "purchase_date": { + "date": "2022-10-31", + "formatted": "Mon Oct 31, 2022", + }, + "age": "1m 18d", + "last_checkout": None, + "expected_checkin": None, + "purchase_cost": "1.599,62", + "checkin_counter": 0, + "checkout_counter": 0, + "requests_counter": 0, + "user_can_checkout": True, + "custom_fields": {}, + "available_actions": { + "checkout": True, + "checkin": True, + "clone": True, + "restore": False, + "update": True, + "delete": True, + }, + }, + ], + "company_b": [ + { + "id": 2598, + "name": "sink", + "asset_tag": "bathroom ", + "serial": "", + "model": { + "id": 2, + "name": "Macbook Air", + }, + "byod": False, + "requestable": False, + "model_number": "4024007136154761", + "eol": None, + "asset_eol_date": None, + "status_label": { + "id": 7, + "name": "Lost/Stolen", + "status_type": "undeployable", + "status_meta": "undeployable", + }, + "category": { + "id": 1, + "name": "Laptops", + }, + "manufacturer": { + "id": 1, + "name": "Apple", + }, + "supplier": None, + "notes": None, + "order_number": None, + "company": None, + "location": None, + "rtd_location": None, + "image": "https://develop.snipeitapp.com/uploads/models/macbookair.jpg", + "qr": "https://develop.snipeitapp.com/uploads/barcodes/qr-bathroom-2598.png", + "alt_barcode": "https://develop.snipeitapp.com/uploads/barcodes/c128-bathroom.png", + "assigned_to": None, + "warranty_months": None, + "warranty_expires": None, + "created_at": { + "datetime": "2024-04-26 09:49:55", + "formatted": "Fri Apr 26, 2024 9:49AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:49:55", + "formatted": "Fri Apr 26, 2024 9:49AM", + }, + "last_audit_date": None, + "next_audit_date": None, + "deleted_at": None, + "purchase_date": None, + "age": "", + "last_checkout": None, + "last_checkin": None, + "expected_checkin": None, + "purchase_cost": None, + "checkin_counter": 0, + "checkout_counter": 0, + "requests_counter": 0, + "user_can_checkout": False, + "book_value": None, + "custom_fields": { + "IMEI": { + "field": "_snipeit_imei_1", + "value": "", + "field_format": "regex:/^[0-9]{15}$/", + "element": "text", + }, + "Phone Number": { + "field": "_snipeit_phone_number_2", + "value": "", + "field_format": "ANY", + "element": "text", + }, + "Test Encrypted": { + "field": "_snipeit_test_encrypted_6", + "value": "", + "field_format": "ANY", + "element": "text", + }, + "Test Checkbox": { + "field": "_snipeit_test_checkbox_7", + "value": "", + "field_format": "ANY", + "element": "checkbox", + }, + "Test Radio": { + "field": "_snipeit_test_radio_8", + "value": "", + "field_format": "ANY", + "element": "radio", + }, + }, + "available_actions": { + "checkout": True, + "checkin": True, + "clone": True, + "restore": False, + "update": True, + "delete": True, + }, + }, + { + "id": 2597, + "name": "", + "asset_tag": "687741510", + "serial": "400bf5a1-da89-3933-96a5-159bcd07b738", + "model": { + "id": 18, + "name": "Ultrasharp U2415", + }, + "byod": False, + "requestable": False, + "model_number": "4929995944077", + "eol": "12 months", + "asset_eol_date": { + "date": "2025-01-16", + "formatted": "Thu Jan 16, 2025", + }, + "status_label": { + "id": 1, + "name": "Ready to Deploy", + "status_type": "deployable", + "status_meta": "deployed", + }, + "category": { + "id": 5, + "name": "Displays", + }, + "manufacturer": { + "id": 3, + "name": "Dell", + }, + "supplier": { + "id": 1, + "name": "Bergstrom and Sons", + }, + "notes": "Created by DB seeder", + "order_number": "28218722", + "company": None, + "location": { + "id": 3, + "name": "West Cecilland", + }, + "rtd_location": { + "id": 3, + "name": "West Cecilland", + }, + "image": "https://develop.snipeitapp.com/uploads/models/ultrasharp.jpg", + "qr": "https://develop.snipeitapp.com/uploads/barcodes/qr-687741510-2597.png", + "alt_barcode": "https://develop.snipeitapp.com/uploads/barcodes/c128-687741510.png", + "assigned_to": { + "id": 47, + "username": "alvera.kuhic", + "name": "Stephany Runolfsson", + "first_name": "Stephany", + "last_name": "Runolfsson", + "email": "nakia.larson@example.com", + "employee_number": "25518", + "type": "user", + }, + "warranty_months": None, + "warranty_expires": None, + "created_at": { + "datetime": "2024-04-26 09:43:07", + "formatted": "Fri Apr 26, 2024 9:43AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:43:39", + "formatted": "Fri Apr 26, 2024 9:43AM", + }, + "last_audit_date": None, + "next_audit_date": None, + "deleted_at": None, + "purchase_date": { + "date": "2024-01-16", + "formatted": "Tue Jan 16, 2024", + }, + "age": "3 months ago", + "last_checkout": None, + "last_checkin": None, + "expected_checkin": None, + "purchase_cost": "214.056,00", + "checkin_counter": 0, + "checkout_counter": 0, + "requests_counter": 0, + "user_can_checkout": False, + "book_value": "160.542,00", + "custom_fields": {}, + "available_actions": { + "checkout": True, + "checkin": True, + "clone": True, + "restore": False, + "update": True, + "delete": False, + }, + }, + ], +} diff --git a/tests/data/snipeit/tenants.py b/tests/data/snipeit/tenants.py new file mode 100644 index 000000000..780d545ef --- /dev/null +++ b/tests/data/snipeit/tenants.py @@ -0,0 +1,8 @@ +TENANTS = { + "company_a": { + "id": "Company A", + }, + "company_b": { + "id": "Company B", + }, +} diff --git a/tests/data/snipeit/users.py b/tests/data/snipeit/users.py new file mode 100644 index 000000000..c1ffb88d1 --- /dev/null +++ b/tests/data/snipeit/users.py @@ -0,0 +1,262 @@ +USERS = { + "company_a": [ + { + "id": 1, + "avatar": "https://develop.snipeitapp.com/uploads/avatars/1.jpg", + "name": "Admin User", + "first_name": "Admin", + "last_name": "User", + "username": "admin", + "remote": False, + "locale": "en-US", + "employee_num": "12832", + "manager": None, + "jobtitle": "Occupational Therapist Aide", + "vip": False, + "phone": "+1.234.432.8715", + "website": None, + "address": "703 Jeramy Parkway Apt. 582\nTitusfurt, SC 30323", + "city": "South Keven", + "state": "MT", + "country": "Tajikistan", + "zip": "22367", + "email": "mcarter@example.net", + "department": { + "id": 3, + "name": "Marketing", + }, + "location": None, + "notes": "Created by DB seeder", + "permissions": { + "superuser": "1", + }, + "activated": True, + "autoassign_licenses": True, + "ldap_import": False, + "two_factor_enrolled": False, + "two_factor_optin": False, + "assets_count": 7, + "licenses_count": 1, + "accessories_count": 0, + "consumables_count": 0, + "company": { + "id": 2, + "name": "Lind, Schroeder and Pacocha", + }, + "created_by": None, + "created_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "start_date": None, + "end_date": None, + "last_login": None, + "deleted_at": None, + "available_actions": { + "update": True, + "delete": False, + "clone": True, + "restore": False, + }, + "groups": None, + }, + { + "id": 2, + "avatar": "https://develop.snipeitapp.com/uploads/avatars/2.jpg", + "name": "Snipe E. Head", + "first_name": "Snipe E.", + "last_name": "Head", + "username": "snipe", + "remote": False, + "locale": "en-US", + "employee_num": "30845", + "manager": None, + "jobtitle": "Transportation Worker", + "vip": False, + "phone": "785-362-4759", + "website": None, + "address": "7535 Bruen Junctions\nNorth Ceciliachester, VA 80961-3508", + "city": "Breitenbergborough", + "state": "ND", + "country": "Gambia", + "zip": "67260-1176", + "email": "snipe@snipe.net", + "department": { + "id": 6, + "name": "Dept of Silly Walks", + }, + "location": None, + "notes": "Created by DB seeder", + "permissions": { + "superuser": "1", + }, + "activated": True, + "autoassign_licenses": True, + "ldap_import": False, + "two_factor_enrolled": False, + "two_factor_optin": False, + "assets_count": 6, + "licenses_count": 1, + "accessories_count": 0, + "consumables_count": 0, + "company": { + "id": 1, + "name": "Beier, Schumm and Upton", + }, + "created_by": None, + "created_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "start_date": None, + "end_date": None, + "last_login": None, + "deleted_at": None, + "available_actions": { + "update": True, + "delete": False, + "clone": True, + "restore": False, + }, + "groups": None, + }, + ], + "company_b": [ + { + "id": 3, + "avatar": "https://develop.snipeitapp.com/uploads/avatars/3.jpg", + "name": "Alison Gianotto", + "first_name": "Alison", + "last_name": "Gianotto", + "username": "agianotto@grokability.com", + "remote": False, + "locale": "en-US", + "employee_num": "9280", + "manager": None, + "jobtitle": "Arbitrator", + "vip": False, + "phone": "(215) 671-8482", + "website": None, + "address": "1770 Walker Forges Suite 580\nPort Lonnyfort, RI 35956-9785", + "city": "East Helen", + "state": "NV", + "country": "Mongolia", + "zip": "15923-4319", + "email": "agianotto@grokability.com", + "department": { + "id": 6, + "name": "Dept of Silly Walks", + }, + "location": None, + "notes": "Created by DB seeder", + "permissions": { + "superuser": "1", + }, + "activated": True, + "autoassign_licenses": True, + "ldap_import": False, + "two_factor_enrolled": False, + "two_factor_optin": False, + "assets_count": 7, + "licenses_count": 1, + "accessories_count": 0, + "consumables_count": 0, + "company": { + "id": 1, + "name": "Beier, Schumm and Upton", + }, + "created_by": None, + "created_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:42:06", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "start_date": None, + "end_date": None, + "last_login": None, + "deleted_at": None, + "available_actions": { + "update": True, + "delete": False, + "clone": True, + "restore": False, + }, + "groups": None, + }, + { + "id": 4, + "avatar": "https://develop.snipeitapp.com/uploads/avatars/4.jpg", + "name": "Afton Kris", + "first_name": "Afton", + "last_name": "Kris", + "username": "boehm.sid", + "remote": False, + "locale": "en-US", + "employee_num": "7457", + "manager": None, + "jobtitle": "Computer Programmer", + "vip": False, + "phone": "478-258-7137", + "website": None, + "address": "6010 Heaney Greens\nNew Soledad, UT 74152", + "city": "Robelborough", + "state": "AK", + "country": "Rwanda", + "zip": "88727-2261", + "email": "sparisian@example.net", + "department": { + "id": 6, + "name": "Dept of Silly Walks", + }, + "location": None, + "notes": "Created by DB seeder", + "permissions": { + "superuser": "1", + }, + "activated": True, + "autoassign_licenses": True, + "ldap_import": False, + "two_factor_enrolled": False, + "two_factor_optin": False, + "assets_count": 4, + "licenses_count": 0, + "accessories_count": 0, + "consumables_count": 0, + "company": { + "id": 4, + "name": "VonRueden-White", + }, + "created_by": None, + "created_at": { + "datetime": "2024-04-26 09:42:05", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "updated_at": { + "datetime": "2024-04-26 09:42:06", + "formatted": "Fri Apr 26, 2024 9:42AM", + }, + "start_date": None, + "end_date": None, + "last_login": None, + "deleted_at": None, + "available_actions": { + "update": True, + "delete": False, + "clone": True, + "restore": False, + }, + "groups": None, + }, + ], +} diff --git a/tests/integration/cartography/intel/snipeit/__init__.py b/tests/integration/cartography/intel/snipeit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/cartography/intel/snipeit/test_snipeit_assets.py b/tests/integration/cartography/intel/snipeit/test_snipeit_assets.py new file mode 100644 index 000000000..f201017f1 --- /dev/null +++ b/tests/integration/cartography/intel/snipeit/test_snipeit_assets.py @@ -0,0 +1,183 @@ +import logging + +import cartography.intel.snipeit +import tests.data.snipeit.assets +import tests.data.snipeit.tenants +import tests.data.snipeit.users +from tests.integration.util import check_nodes +from tests.integration.util import check_rels + +logger = logging.getLogger(__name__) + + +def test_load_snipeit_assets_relationship(neo4j_session): + # Arrange + TEST_UPDATE_TAG = 1234 + TEST_snipeit_TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_a"]["id"] + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + + # Load test users for the relationship + data = tests.data.snipeit.users.USERS['company_a'] + cartography.intel.snipeit.user.load_users( + neo4j_session, + common_job_parameters, + data, + ) + + data = tests.data.snipeit.assets.ASSETS['company_a'] + + # Act + cartography.intel.snipeit.asset.load_assets( + neo4j_session, + common_job_parameters, + data, + ) + + # Assert + + # Make sure the expected Tenant is created + expected_nodes = { + ('Company A',), + } + check_nodes( + neo4j_session, + 'snipeitTenant', + ['id'], + ) + + # Make sure the expected assets are created + expected_nodes = { + (1373, "C02ZJ48XXXXX"), + (1372, "72ec94a8-b6dc-37f1-b2a9-0907806e8db7"), + } + assert check_nodes( + neo4j_session, + "SnipeitAsset", + ["id", "serial"], + ) == expected_nodes + + # Make sure the expected relationships are created + expected_nodes_relationships = { + ('Company A', "C02ZJ48XXXXX"), + ('Company A', "72ec94a8-b6dc-37f1-b2a9-0907806e8db7"), + } + assert check_rels( + neo4j_session, + 'SnipeitTenant', + 'id', + 'SnipeitAsset', + 'serial', + 'HAS_ASSET', + rel_direction_right=True, + ) == expected_nodes_relationships + + expected_nodes_relationships = { + ("mcarter@example.net", "C02ZJ48XXXXX"), + } + assert check_rels( + neo4j_session, + 'SnipeitUser', + 'email', + 'SnipeitAsset', + 'serial', + 'HAS_CHECKED_OUT', + rel_direction_right=True, + ) == expected_nodes_relationships + + # Cleanup test data + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG + 1234, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.asset.cleanup( + neo4j_session, + common_job_parameters, + ) + + +def test_cleanup_snipeit_assets(neo4j_session): + # Arrange + TEST_UPDATE_TAG = 1234 + TEST_snipeit_TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_a"]["id"] + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + data = tests.data.snipeit.assets.ASSETS['company_a'] + + # Act + cartography.intel.snipeit.asset.load_assets( + neo4j_session, + common_job_parameters, + data, + ) + + # Arrange: load in an unrelated data with different UPDATE_TAG + UNRELATED_UPDATE_TAG = TEST_UPDATE_TAG + 1 + TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_b"]["id"] + common_job_parameters = { + "UPDATE_TAG": UNRELATED_UPDATE_TAG, + "TENANT_ID": TENANT_ID, + } + data = tests.data.snipeit.assets.ASSETS["company_b"] + + cartography.intel.snipeit.asset.load_assets( + neo4j_session, + common_job_parameters, + data, + ) + + # # [Pre-test] Assert + + # [Pre-test] Assert that the unrelated data exists + expected_nodes_relationships = { + ("Company A", 1373), + ('Company A', 1372), + ('Company B', 2598), + ('Company B', 2597), + + } + assert check_rels( + neo4j_session, + 'SnipeitTenant', + 'id', + 'SnipeitAsset', + 'id', + 'HAS_ASSET', + rel_direction_right=True, + ) == expected_nodes_relationships + + # Act: run the cleanup job to remove all nodes except the unrelated data + common_job_parameters = { + "UPDATE_TAG": UNRELATED_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.asset.cleanup( + neo4j_session, + common_job_parameters, + ) + + # Assert: Expect unrelated data nodes remains + expected_nodes_unrelated = { + (2597,), + (2598,), + } + + assert check_nodes( + neo4j_session, + "SnipeitAsset", + ["id"], + ) == expected_nodes_unrelated + + # Cleanup all test data + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG + 9999, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.asset.cleanup( + neo4j_session, + common_job_parameters, + ) diff --git a/tests/integration/cartography/intel/snipeit/test_snipeit_users.py b/tests/integration/cartography/intel/snipeit/test_snipeit_users.py new file mode 100644 index 000000000..bdd3e9888 --- /dev/null +++ b/tests/integration/cartography/intel/snipeit/test_snipeit_users.py @@ -0,0 +1,159 @@ +import logging + +import cartography.intel.snipeit +import tests.data.snipeit.tenants +import tests.data.snipeit.users +from tests.integration.util import check_nodes +from tests.integration.util import check_rels + +logger = logging.getLogger(__name__) + + +def test_load_snipeit_user_relationship(neo4j_session): + # Arrange + TEST_UPDATE_TAG = 1234 + TEST_snipeit_TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_a"]["id"] + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + data = tests.data.snipeit.users.USERS['company_a'] + + # Act + cartography.intel.snipeit.user.load_users( + neo4j_session, + common_job_parameters, + data, + ) + + # Assert + + # Make sure the expected Tenant is created + expected_nodes = { + ('Company A',), + } + check_nodes( + neo4j_session, + 'SnipeitTenant', + ['id'], + ) + + # Make sure the expected Devices are created + expected_nodes = { + (1, 'mcarter@example.net'), + (2, 'snipe@snipe.net'), + } + assert check_nodes( + neo4j_session, + "SnipeitUser", + ["id", "email"], + ) == expected_nodes + + # Make sure the expected relationships are created + expected_nodes_relationships = { + ('Company A', 1), + ('Company A', 2), + } + assert check_rels( + neo4j_session, + 'SnipeitTenant', + 'id', + 'SnipeitUser', + 'id', + 'HAS_USER', + rel_direction_right=True, + ) == expected_nodes_relationships + + # Cleanup test data + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG + 1234, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.user.cleanup( + neo4j_session, + common_job_parameters, + ) + + +def test_cleanup_snipeit_users(neo4j_session): + # Arrange + TEST_UPDATE_TAG = 1234 + TEST_snipeit_TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_a"]["id"] + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + data = tests.data.snipeit.users.USERS['company_a'] + + # Act + cartography.intel.snipeit.user.load_users( + neo4j_session, + common_job_parameters, + data, + ) + + # Arrange: load in an unrelated data with different UPDATE_TAG + UNRELATED_UPDATE_TAG = TEST_UPDATE_TAG + 1 + TENANT_ID = tests.data.snipeit.tenants.TENANTS["company_b"]["id"] + common_job_parameters = { + "UPDATE_TAG": UNRELATED_UPDATE_TAG, + "TENANT_ID": TENANT_ID, + } + data = tests.data.snipeit.users.USERS['company_b'] + + cartography.intel.snipeit.user.load_users( + neo4j_session, + common_job_parameters, + data, + ) + + # # [Pre-test] Assert + + # [Pre-test] Assert that the related and unrelated data exists + expected_nodes_relationships = { + ('Company A', 1), + ('Company A', 2), + ('Company B', 3), + ('Company B', 4), + } + assert check_rels( + neo4j_session, + 'SnipeitTenant', + 'id', + 'SnipeitUser', + 'id', + 'HAS_USER', + rel_direction_right=True, + ) == expected_nodes_relationships + + # Act: run the cleanup job to remove all nodes except the unrelated data + common_job_parameters = { + "UPDATE_TAG": UNRELATED_UPDATE_TAG, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.user.cleanup( + neo4j_session, + common_job_parameters, + ) + + # Assert: Expect unrelated data nodes remains + expected_nodes_unrelated = { + (3, "agianotto@grokability.com"), + (4, "sparisian@example.net"), + } + + assert check_nodes( + neo4j_session, + "SnipeitUser", + ["id", "email"], + ) == expected_nodes_unrelated + + # Cleanup test data + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG + 9999, + "TENANT_ID": TEST_snipeit_TENANT_ID, + } + cartography.intel.snipeit.user.cleanup( + neo4j_session, + common_job_parameters, + )