From fba3f7ca7d0114bf62e7bd4c7ad83907776af91a Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 13 Jan 2023 17:01:13 +0100 Subject: [PATCH 01/18] add lastpass source --- README.md | 1 + cartography/cli.py | 28 ++++++ cartography/config.py | 8 ++ cartography/data/indexes.cypher | 3 + .../jobs/analysis/lastpass_human_link.json | 13 +++ .../jobs/cleanup/lastpass_import_cleanup.json | 8 ++ cartography/intel/lastpass/__init__.py | 39 +++++++++ cartography/intel/lastpass/users.py | 85 +++++++++++++++++++ cartography/sync.py | 2 + docs/root/modules/lastpass/config.md | 9 ++ docs/root/modules/lastpass/index.rst | 12 +++ docs/root/modules/lastpass/schema.md | 45 ++++++++++ tests/data/lastpass/__init__.py | 0 tests/data/lastpass/users.py | 24 ++++++ .../cartography/intel/lastpass/__init__.py | 0 .../cartography/intel/lastpass/test_users.py | 33 +++++++ 16 files changed, 310 insertions(+) create mode 100644 cartography/data/jobs/analysis/lastpass_human_link.json create mode 100644 cartography/data/jobs/cleanup/lastpass_import_cleanup.json create mode 100644 cartography/intel/lastpass/__init__.py create mode 100644 cartography/intel/lastpass/users.py create mode 100644 docs/root/modules/lastpass/config.md create mode 100644 docs/root/modules/lastpass/index.rst create mode 100644 docs/root/modules/lastpass/schema.md create mode 100644 tests/data/lastpass/__init__.py create mode 100644 tests/data/lastpass/users.py create mode 100644 tests/integration/cartography/intel/lastpass/__init__.py create mode 100644 tests/integration/cartography/intel/lastpass/test_users.py diff --git a/README.md b/README.md index ad07af837..9636001f9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Start [here](https://lyft.github.io/cartography/install.html). - [PagerDuty](https://lyft.github.io/cartography/modules/pagerduty/index.html) - Users, teams, services, schedules, escalation policies, integrations, vendors - [Crowdstrike Falcon](https://lyft.github.io/cartography/modules/crowdstrike/index.html) - Hosts, Spotlight vulnerabilites, CVEs - [NIST CVE](https://lyft.github.io/cartography/modules/cve/index.html) - Common Vulnerabilities and Exposures (CVE) data from NIST database +- [Lastpass](https://lyft.github.io/cartography/modules/lastpass/index.html) - users ## 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. diff --git a/cartography/cli.py b/cartography/cli.py index 3d1b827ea..37a704fbe 100644 --- a/cartography/cli.py +++ b/cartography/cli.py @@ -403,6 +403,22 @@ def _build_parser(self): 'The crowdstrike URL, if using self-hosted. Defaults to the public crowdstrike API URL otherwise.' ), ) + parser.add_argument( + '--lastpass-cid-env-var', + type=str, + default=None, + help=( + 'The name of environment variable containing the Lastpass CID for authentication.' + ), + ) + parser.add_argument( + '--lastpass-provhash-env-var', + type=str, + default=None, + help=( + 'The name of environment variable containing the Lastpass provhash for authentication.' + ), + ) return parser def main(self, argv: str) -> int: @@ -532,6 +548,18 @@ def main(self, argv: str) -> int: else: config.crowdstrike_client_secret = None + # Lastpass config + if config.lastpass_cid_env_var: + logger.debug(f"Reading CID for Lastpass from environment variable {config.lastpass_cid_env_var}") + config.lastpass_cid = os.environ.get(config.lastpass_cid_env_var) + else: + config.lastpass_cid = None + if config.lastpass_provhash_env_var: + logger.debug(f"Reading provhash for Lastpass from environment variable {config.lastpass_provhash_env_var}") + config.lastpass_provhash = os.environ.get(config.lastpass_provhash_env_var) + else: + config.lastpass_provhash = None + # Run cartography try: return cartography.sync.run_with_config(self.sync, config) diff --git a/cartography/config.py b/cartography/config.py index a323c8a4d..2a0e153bc 100644 --- a/cartography/config.py +++ b/cartography/config.py @@ -81,6 +81,10 @@ class Config: :param pagerduty_request_timeout: Seconds to timeout for pagerduty session requests. Optional :type: nist_cve_url: str :param nist_cve_url: NIST CVE data provider base URI, e.g. https://nvd.nist.gov/feeds/json/cve/1.1. Optional. + :type lastpass_cid: str + :param lastpass_cid: Lastpass account ID. Optional. + :type lastpass_provhash: str + :param lastpass_provhash: Lastpass API KEY. Optional. """ def __init__( @@ -124,6 +128,8 @@ def __init__( crowdstrike_client_id=None, crowdstrike_client_secret=None, crowdstrike_api_url=None, + lastpass_cid=None, + lastpass_provhash=None, ): self.neo4j_uri = neo4j_uri self.neo4j_user = neo4j_user @@ -164,3 +170,5 @@ def __init__( self.crowdstrike_client_id = crowdstrike_client_id self.crowdstrike_client_secret = crowdstrike_client_secret self.crowdstrike_api_url = crowdstrike_api_url + self.lastpass_cid = lastpass_cid + self.lastpass_provhash = lastpass_provhash diff --git a/cartography/data/indexes.cypher b/cartography/data/indexes.cypher index 4db76ed08..87456c54b 100644 --- a/cartography/data/indexes.cypher +++ b/cartography/data/indexes.cypher @@ -455,3 +455,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:KubernetesSecret) ON (n.lastupdated); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.id); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.name); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.lastupdated); +CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.id); +CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.email); +CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.lastupdated); diff --git a/cartography/data/jobs/analysis/lastpass_human_link.json b/cartography/data/jobs/analysis/lastpass_human_link.json new file mode 100644 index 000000000..ab90582fe --- /dev/null +++ b/cartography/data/jobs/analysis/lastpass_human_link.json @@ -0,0 +1,13 @@ +{ + "statements": [ + { + "query": "MATCH (human:Human), (user:LastpassUser) WHERE human.email = user.email MERGE (human)-[r:IDENTITY_LASTPASS]->(user) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG", + "iterative": false + }, + { + "query": "MATCH (:Human)-[r:IDENTITY_LASTPASS]->(:LastpassUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r) return COUNT(*) as TotalCompleted", + "iterative": true, + "iterationsize": 100 + }], + "name": "Lastpass user map to Human" +} diff --git a/cartography/data/jobs/cleanup/lastpass_import_cleanup.json b/cartography/data/jobs/cleanup/lastpass_import_cleanup.json new file mode 100644 index 000000000..4a2bc32c9 --- /dev/null +++ b/cartography/data/jobs/cleanup/lastpass_import_cleanup.json @@ -0,0 +1,8 @@ +{ + "statements": [{ + "query": "MATCH (n:LastpassUser) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + }], + "name": "cleanup LastpassUser" +} diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py new file mode 100644 index 000000000..0c9a36409 --- /dev/null +++ b/cartography/intel/lastpass/__init__.py @@ -0,0 +1,39 @@ +import logging + +import neo4j + +import cartography.intel.lastpass.users +from cartography.config import Config +from cartography.util import run_cleanup_job +from cartography.util import timeit + +logger = logging.getLogger(__name__) + + +@timeit +def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> None: + """ + If this module is configured, perform ingestion of Lastpass data. Otherwise warn and exit + :param neo4j_session: Neo4J session for database interface + :param config: A cartography.config object + :return: None + """ + + if not config.lastpass_cid or not config.lastpass_provhash: + logger.info( + 'Lastpass import is not configured - skipping this module. ' + 'See docs to configure.', + ) + return + + common_job_parameters = { + "UPDATE_TAG": config.update_tag, + } + + cartography.intel.lastpass.users.sync(neo4j_session, config.update_tag, config.lastpass_cid, config.lastpass_provhash) + + run_cleanup_job( + "lastpass_import_cleanup.json", + neo4j_session, + common_job_parameters, + ) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py new file mode 100644 index 000000000..a0877e41b --- /dev/null +++ b/cartography/intel/lastpass/users.py @@ -0,0 +1,85 @@ +import logging +from typing import Dict, List + +import neo4j +from requests import Session +from dateutil import parser as dt_parse + +from cartography.util import timeit + +logger = logging.getLogger(__name__) + + +@timeit +def sync( + neo4j_session: neo4j.Session, + update_tag: int, + lastpass_cid: str, + lastpass_provhash: str +) -> None: + users = get(lastpass_cid, lastpass_provhash) + formated_users = transform(users) + load(neo4j_session, formated_users, update_tag) + + +@timeit +def get(lastpass_cid: str, lastpass_provhash: str) -> dict: + payload = { + 'cid': lastpass_cid, + 'provhash': lastpass_provhash, + 'cmd': 'getuserdata', + 'data': None + } + session = Session() + req = session.post('https://lastpass.com/enterpriseapi.php', data=payload, timeout=20) + req.raise_for_status() + return req.json() + +@timeit +def transform(api_result: dict) -> List[Dict]: + result: List[dict] = [] + for uid, user in api_result['Users'].items(): + n_user = user.copy() + n_user['id'] = int(uid) + n_user['created'] = dt_parse.parse(user['created']) + n_user['last_pw_change'] = dt_parse.parse(user['last_pw_change']) + n_user['last_login'] = dt_parse.parse(user['last_login']) + result.append(n_user) + return result + +def load( + neo4j_session: neo4j.Session, + data: List[Dict], + update_tag: int, +) -> None: + + query = """ + UNWIND $UserData as user + MERGE (u:LastpassUser{id: user.id}) + ON CREATE set u.firstseen = timestamp() + SET u.lastupdated = $UpdateTag, + u.id = user.id, + u.name = user.fullname, + u.email = user.username, + u.created = user.created, + u.last_pw_change = user.last_pw_change, + u.last_login = user.last_login, + u.neverloggedin = user.neverloggedin, + u.disabled = user.disabled, + u.admin = user.admin, + u.totalscore = user.totalscore, + u.mpstrength = user.mpstrength, + u.sites = user.sites, + u.notes = user.notes, + u.formfills = user.formfills, + u.applications = user.applications, + u.attachments = user.attachments, + u.password_reset_required = user.password_reset_required, + u.multifactor = user.multifactor + """ + + neo4j_session.run( + query, + UserData=data, + UpdateTag=update_tag, + ) diff --git a/cartography/sync.py b/cartography/sync.py index 4ac02593c..4566ced2c 100644 --- a/cartography/sync.py +++ b/cartography/sync.py @@ -23,6 +23,7 @@ import cartography.intel.github import cartography.intel.gsuite import cartography.intel.kubernetes +import cartography.intel.lastpass import cartography.intel.oci import cartography.intel.okta from cartography.config import Config @@ -185,6 +186,7 @@ def build_default_sync() -> Sync: ('github', cartography.intel.github.start_github_ingestion), ('digitalocean', cartography.intel.digitalocean.start_digitalocean_ingestion), ('kubernetes', cartography.intel.kubernetes.start_k8s_ingestion), + ('lastpass', cartography.intel.lastpass.start_lastpass_ingestion), ('analysis', cartography.intel.analysis.run), ]) return sync diff --git a/docs/root/modules/lastpass/config.md b/docs/root/modules/lastpass/config.md new file mode 100644 index 000000000..3d8ebe753 --- /dev/null +++ b/docs/root/modules/lastpass/config.md @@ -0,0 +1,9 @@ +## Lastpass Configuration + +.. _lastpass_config: + +Follow these steps to analyze Lastpass objects with Cartography. + +1. Prepare your Lastpass CID & ProvHash key. + 1. Get your CID (account number) and ProvHash from Lastpass [Where can I find the CID and API secret?](https://support.lastpass.com/help/where-can-i-find-the-cid-and-api-secret) + 1. Populate an environment variable with the CID and Provhash. You can pass the environment variable name via CLI with the `--lastpass-cid-env-var` and `--lastpass-provhash-env-var` parameter. diff --git a/docs/root/modules/lastpass/index.rst b/docs/root/modules/lastpass/index.rst new file mode 100644 index 000000000..217890aca --- /dev/null +++ b/docs/root/modules/lastpass/index.rst @@ -0,0 +1,12 @@ +Lastpass +######## + +The lastpass module has the following coverage: + +* Users + +.. toctree:: + :hidden: + :glob: + + * \ No newline at end of file diff --git a/docs/root/modules/lastpass/schema.md b/docs/root/modules/lastpass/schema.md new file mode 100644 index 000000000..d8f7c1adb --- /dev/null +++ b/docs/root/modules/lastpass/schema.md @@ -0,0 +1,45 @@ +## Lastpass Schema + +.. _lastpass_schema: + + +### Human + +Lastpass use Human node as pivot with other Identity Providers (GSuite, GitHub ...) + +Human nodes are not created by Lastpass module, link is made using analysis job. + + +#### Relationships + +- Human as an access to Lastpass + ``` + (Human)-[IDENTITY_LASTPASS]->(LastpassUser) + ``` + +### LastpassUser + +Representation of a single User in Lastpass + +| Field | Description | +|-------|--------------| +| firstseen| Timestamp of when a sync job first created this node | +| lastupdated | Timestamp of the last time the node was updated | +| id | Lastpass ID | +| name | Full name of the user | +| email | User email | +| created | Timestamp of when the account was created | +| last_pw_change | Timestamp of the last master password change | +| last_login | Timestamp of the last login | +| neverloggedin | Flag indicating the user never logged in | +| disabled | Flag indicating accout is disabled | +| admin | Flag for admin account | +| totalscore | Lastpass security score (max 100) | +| mpstrength | Master password strenght (max 100) | +| sites | Number of site credentials stored | +| notes | Number of secured notes stored | +| formfills | Number of forms stored | +| applications | Number of applications (mobile) stored | +| attachments | Number of file attachments stored | +| password_reset_required | Flag indicating user requested password reset | +| multifactor | MFA method (null if None) | \ No newline at end of file diff --git a/tests/data/lastpass/__init__.py b/tests/data/lastpass/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/lastpass/users.py b/tests/data/lastpass/users.py new file mode 100644 index 000000000..985422998 --- /dev/null +++ b/tests/data/lastpass/users.py @@ -0,0 +1,24 @@ +LASTPASS_USERS = { + 'Users': { + 123456: { + "username": "john.doe@domain.tld", + "fullname": "John Doe", + "mpstrength": "100", + "created": "2022-08-31 04:45:15", + "last_pw_change": "2022-08-31 06:03:16", + "last_login": "2023-01-12 09:43:34", + "neverloggedin": False, + "disabled": False, + "admin": True, + "totalscore": "83.2", + "multifactor": "lastpassauth", + "duousername": "john.doe@domain.tld", + "sites": 25, + "notes": 1, + "formfills": 0, + "applications": 0, + "attachments": 0, + "password_reset_required": False + } + } +} \ No newline at end of file diff --git a/tests/integration/cartography/intel/lastpass/__init__.py b/tests/integration/cartography/intel/lastpass/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py new file mode 100644 index 000000000..cce362492 --- /dev/null +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -0,0 +1,33 @@ +import cartography.intel.lastpass.users +import tests.data.lastpass.users + + +TEST_UPDATE_TAG = 123456789 + + +def test_load_lastpass_users(neo4j_session): + + data = tests.data.lastpass.users.LASTPASS_USERS + formatted_data = cartography.intel.lastpass.users.transform(data) + cartography.intel.lastpass.users.load( + neo4j_session, + formatted_data, + TEST_UPDATE_TAG, + ) + + # Ensure users got loaded + nodes = neo4j_session.run( + """ + MATCH (e:LastpassUser) RETURN e.id, e.email; + """, + ) + expected_nodes = { + (123456, 'john.doe@domain.tld') + } + actual_nodes = { + ( + n['e.id'], + n['e.email'] + ) for n in nodes + } + assert actual_nodes == expected_nodes From 3e42b8fc22be2aa04e3b5047576465960d202b6e Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Wed, 18 Jan 2023 11:05:22 +0100 Subject: [PATCH 02/18] migrate to node schema --- cartography/intel/lastpass/__init__.py | 7 ++- cartography/intel/lastpass/schema.py | 39 ++++++++++++++ cartography/intel/lastpass/users.py | 54 +++++++------------ docs/root/modules/lastpass/index.rst | 2 +- docs/root/modules/lastpass/schema.md | 2 +- tests/data/lastpass/users.py | 42 +++++++-------- .../cartography/intel/lastpass/test_users.py | 4 +- 7 files changed, 89 insertions(+), 61 deletions(-) create mode 100644 cartography/intel/lastpass/schema.py diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index 0c9a36409..5dcb7ce91 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -30,7 +30,12 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No "UPDATE_TAG": config.update_tag, } - cartography.intel.lastpass.users.sync(neo4j_session, config.update_tag, config.lastpass_cid, config.lastpass_provhash) + cartography.intel.lastpass.users.sync( + neo4j_session, + config.update_tag, + config.lastpass_cid, + config.lastpass_provhash, + ) run_cleanup_job( "lastpass_import_cleanup.json", diff --git a/cartography/intel/lastpass/schema.py b/cartography/intel/lastpass/schema.py new file mode 100644 index 000000000..09954a2b8 --- /dev/null +++ b/cartography/intel/lastpass/schema.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass + +from cartography.graph.model import CartographyNodeProperties +from cartography.graph.model import CartographyNodeSchema +from cartography.graph.model import CartographyRelProperties +from cartography.graph.model import CartographyRelSchema +from cartography.graph.model import LinkDirection +from cartography.graph.model import make_target_node_matcher +from cartography.graph.model import PropertyRef +from cartography.graph.model import TargetNodeMatcher + + +@dataclass(frozen=True) +class LastpassUserNodeProperties(CartographyNodeProperties): + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + name: PropertyRef = PropertyRef('fullname') + email: PropertyRef = PropertyRef('username') + created: PropertyRef = PropertyRef('created') + last_pw_change: PropertyRef = PropertyRef('last_pw_change') + last_login: PropertyRef = PropertyRef('last_login') + neverloggedin: PropertyRef = PropertyRef('neverloggedin') + disabled: PropertyRef = PropertyRef('disabled') + admin: PropertyRef = PropertyRef('admin') + totalscore: PropertyRef = PropertyRef('totalscore') + mpstrength: PropertyRef = PropertyRef('mpstrength') + sites: PropertyRef = PropertyRef('sites') + notes: PropertyRef = PropertyRef('notes') + formfills: PropertyRef = PropertyRef('formfills') + applications: PropertyRef = PropertyRef('applications') + attachments: PropertyRef = PropertyRef('attachments') + password_reset_required: PropertyRef = PropertyRef('password_reset_required') + multifactor: PropertyRef = PropertyRef('multifactor') + + +@dataclass(frozen=True) +class LastpassUserSchema(CartographyNodeSchema): + label: str = 'LastpassUser' + properties: LastpassUserNodeProperties = LastpassUserNodeProperties() # An object representing all properties on the EMR Cluster node diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index a0877e41b..63d5f3252 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -1,10 +1,14 @@ import logging -from typing import Dict, List +from typing import Dict +from typing import List import neo4j -from requests import Session from dateutil import parser as dt_parse +from requests import Session +from cartography.client.core.tx import load_graph_data +from cartography.graph.querybuilder import build_ingestion_query +from cartography.intel.lastpass.schema import LastpassUserSchema from cartography.util import timeit logger = logging.getLogger(__name__) @@ -15,7 +19,7 @@ def sync( neo4j_session: neo4j.Session, update_tag: int, lastpass_cid: str, - lastpass_provhash: str + lastpass_provhash: str, ) -> None: users = get(lastpass_cid, lastpass_provhash) formated_users = transform(users) @@ -28,58 +32,38 @@ def get(lastpass_cid: str, lastpass_provhash: str) -> dict: 'cid': lastpass_cid, 'provhash': lastpass_provhash, 'cmd': 'getuserdata', - 'data': None + 'data': None, } session = Session() req = session.post('https://lastpass.com/enterpriseapi.php', data=payload, timeout=20) req.raise_for_status() return req.json() + @timeit def transform(api_result: dict) -> List[Dict]: result: List[dict] = [] for uid, user in api_result['Users'].items(): n_user = user.copy() n_user['id'] = int(uid) - n_user['created'] = dt_parse.parse(user['created']) - n_user['last_pw_change'] = dt_parse.parse(user['last_pw_change']) - n_user['last_login'] = dt_parse.parse(user['last_login']) + n_user['created'] = int(dt_parse.parse(user['created']).timestamp() * 1000) + n_user['last_pw_change'] = int(dt_parse.parse(user['last_pw_change']).timestamp() * 1000) + n_user['last_login'] = int(dt_parse.parse(user['last_login']).timestamp() * 1000) result.append(n_user) return result + def load( neo4j_session: neo4j.Session, data: List[Dict], update_tag: int, ) -> None: - query = """ - UNWIND $UserData as user - MERGE (u:LastpassUser{id: user.id}) - ON CREATE set u.firstseen = timestamp() - SET u.lastupdated = $UpdateTag, - u.id = user.id, - u.name = user.fullname, - u.email = user.username, - u.created = user.created, - u.last_pw_change = user.last_pw_change, - u.last_login = user.last_login, - u.neverloggedin = user.neverloggedin, - u.disabled = user.disabled, - u.admin = user.admin, - u.totalscore = user.totalscore, - u.mpstrength = user.mpstrength, - u.sites = user.sites, - u.notes = user.notes, - u.formfills = user.formfills, - u.applications = user.applications, - u.attachments = user.attachments, - u.password_reset_required = user.password_reset_required, - u.multifactor = user.multifactor - """ + ingestion_query = build_ingestion_query(LastpassUserSchema()) - neo4j_session.run( - query, - UserData=data, - UpdateTag=update_tag, + load_graph_data( + neo4j_session, + ingestion_query, + data, + lastupdated=update_tag, ) diff --git a/docs/root/modules/lastpass/index.rst b/docs/root/modules/lastpass/index.rst index 217890aca..96a036e94 100644 --- a/docs/root/modules/lastpass/index.rst +++ b/docs/root/modules/lastpass/index.rst @@ -9,4 +9,4 @@ The lastpass module has the following coverage: :hidden: :glob: - * \ No newline at end of file + * diff --git a/docs/root/modules/lastpass/schema.md b/docs/root/modules/lastpass/schema.md index d8f7c1adb..cbbff9a5b 100644 --- a/docs/root/modules/lastpass/schema.md +++ b/docs/root/modules/lastpass/schema.md @@ -42,4 +42,4 @@ Representation of a single User in Lastpass | applications | Number of applications (mobile) stored | | attachments | Number of file attachments stored | | password_reset_required | Flag indicating user requested password reset | -| multifactor | MFA method (null if None) | \ No newline at end of file +| multifactor | MFA method (null if None) | diff --git a/tests/data/lastpass/users.py b/tests/data/lastpass/users.py index 985422998..a6217227a 100644 --- a/tests/data/lastpass/users.py +++ b/tests/data/lastpass/users.py @@ -1,24 +1,24 @@ LASTPASS_USERS = { 'Users': { 123456: { - "username": "john.doe@domain.tld", - "fullname": "John Doe", - "mpstrength": "100", - "created": "2022-08-31 04:45:15", - "last_pw_change": "2022-08-31 06:03:16", - "last_login": "2023-01-12 09:43:34", - "neverloggedin": False, - "disabled": False, - "admin": True, - "totalscore": "83.2", - "multifactor": "lastpassauth", - "duousername": "john.doe@domain.tld", - "sites": 25, - "notes": 1, - "formfills": 0, - "applications": 0, - "attachments": 0, - "password_reset_required": False - } - } -} \ No newline at end of file + "username": "john.doe@domain.tld", + "fullname": "John Doe", + "mpstrength": "100", + "created": "2022-08-31 04:45:15", + "last_pw_change": "2022-08-31 06:03:16", + "last_login": "2023-01-12 09:43:34", + "neverloggedin": False, + "disabled": False, + "admin": True, + "totalscore": "83.2", + "multifactor": "lastpassauth", + "duousername": "john.doe@domain.tld", + "sites": 25, + "notes": 1, + "formfills": 0, + "applications": 0, + "attachments": 0, + "password_reset_required": False, + }, + }, +} diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py index cce362492..9350b3dc2 100644 --- a/tests/integration/cartography/intel/lastpass/test_users.py +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -22,12 +22,12 @@ def test_load_lastpass_users(neo4j_session): """, ) expected_nodes = { - (123456, 'john.doe@domain.tld') + (123456, 'john.doe@domain.tld'), } actual_nodes = { ( n['e.id'], - n['e.email'] + n['e.email'], ) for n in nodes } assert actual_nodes == expected_nodes From db1d4eea7f93000f39ff4baaaad90c6112e816a7 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Wed, 18 Jan 2023 11:12:06 +0100 Subject: [PATCH 03/18] code cleanup --- cartography/intel/lastpass/schema.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cartography/intel/lastpass/schema.py b/cartography/intel/lastpass/schema.py index 09954a2b8..4bd6436f2 100644 --- a/cartography/intel/lastpass/schema.py +++ b/cartography/intel/lastpass/schema.py @@ -2,12 +2,7 @@ from cartography.graph.model import CartographyNodeProperties from cartography.graph.model import CartographyNodeSchema -from cartography.graph.model import CartographyRelProperties -from cartography.graph.model import CartographyRelSchema -from cartography.graph.model import LinkDirection -from cartography.graph.model import make_target_node_matcher from cartography.graph.model import PropertyRef -from cartography.graph.model import TargetNodeMatcher @dataclass(frozen=True) @@ -36,4 +31,4 @@ class LastpassUserNodeProperties(CartographyNodeProperties): @dataclass(frozen=True) class LastpassUserSchema(CartographyNodeSchema): label: str = 'LastpassUser' - properties: LastpassUserNodeProperties = LastpassUserNodeProperties() # An object representing all properties on the EMR Cluster node + properties: LastpassUserNodeProperties = LastpassUserNodeProperties() From 90f82f1456fc602aa39e6176a827fb9741dab3f2 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Thu, 19 Jan 2023 09:06:55 +0100 Subject: [PATCH 04/18] fix analysis --- cartography/data/jobs/analysis/lastpass_human_link.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cartography/data/jobs/analysis/lastpass_human_link.json b/cartography/data/jobs/analysis/lastpass_human_link.json index ab90582fe..5b510a727 100644 --- a/cartography/data/jobs/analysis/lastpass_human_link.json +++ b/cartography/data/jobs/analysis/lastpass_human_link.json @@ -5,7 +5,7 @@ "iterative": false }, { - "query": "MATCH (:Human)-[r:IDENTITY_LASTPASS]->(:LastpassUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r) return COUNT(*) as TotalCompleted", + "query": "MATCH (:Human)-[r:IDENTITY_LASTPASS]->(:LastpassUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DETACH DELETE (r) return COUNT(*) as TotalCompleted", "iterative": true, "iterationsize": 100 }], From e0e6ddd4b5091e2f9d3b96c61e213b55e4128c16 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Tue, 31 Jan 2023 14:38:36 +0100 Subject: [PATCH 05/18] fix: parsing a none existing last login date --- cartography/intel/lastpass/users.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 63d5f3252..d0531e467 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -48,7 +48,10 @@ def transform(api_result: dict) -> List[Dict]: n_user['id'] = int(uid) n_user['created'] = int(dt_parse.parse(user['created']).timestamp() * 1000) n_user['last_pw_change'] = int(dt_parse.parse(user['last_pw_change']).timestamp() * 1000) - n_user['last_login'] = int(dt_parse.parse(user['last_login']).timestamp() * 1000) + try: + n_user['last_login'] = int(dt_parse.parse(user['last_login']).timestamp() * 1000) + except dt_parse.ParserError: + pass result.append(n_user) return result From 12392287967405404de394919166a824f4eef621 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Wed, 1 Feb 2023 09:11:09 +0100 Subject: [PATCH 06/18] improve date parsing --- cartography/intel/lastpass/users.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index d0531e467..7345fa534 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -46,12 +46,10 @@ def transform(api_result: dict) -> List[Dict]: for uid, user in api_result['Users'].items(): n_user = user.copy() n_user['id'] = int(uid) - n_user['created'] = int(dt_parse.parse(user['created']).timestamp() * 1000) - n_user['last_pw_change'] = int(dt_parse.parse(user['last_pw_change']).timestamp() * 1000) - try: - n_user['last_login'] = int(dt_parse.parse(user['last_login']).timestamp() * 1000) - except dt_parse.ParserError: - pass + for k in ('created', 'last_pw_change', 'last_login'): + if n_user[k] == '': + n_user.pop(k) + n_user[k] = int(dt_parse.parse(user[k]).timestamp() * 1000) result.append(n_user) return result From c4c8260db42c6c89dfeb9c84f040082ed8c289d8 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Thu, 2 Feb 2023 12:00:29 +0100 Subject: [PATCH 07/18] fix date parsing issue --- cartography/intel/lastpass/users.py | 11 ++++++----- tests/data/lastpass/users.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 7345fa534..16ce628e0 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -1,4 +1,5 @@ import logging +from typing import Any from typing import Dict from typing import List @@ -12,6 +13,8 @@ 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 @@ -27,7 +30,7 @@ def sync( @timeit -def get(lastpass_cid: str, lastpass_provhash: str) -> dict: +def get(lastpass_cid: str, lastpass_provhash: str) -> Dict[str, Any]: payload = { 'cid': lastpass_cid, 'provhash': lastpass_provhash, @@ -35,7 +38,7 @@ def get(lastpass_cid: str, lastpass_provhash: str) -> dict: 'data': None, } session = Session() - req = session.post('https://lastpass.com/enterpriseapi.php', data=payload, timeout=20) + req = session.post('https://lastpass.com/enterpriseapi.php', data=payload, timeout=_TIMEOUT) req.raise_for_status() return req.json() @@ -47,9 +50,7 @@ def transform(api_result: dict) -> List[Dict]: n_user = user.copy() n_user['id'] = int(uid) for k in ('created', 'last_pw_change', 'last_login'): - if n_user[k] == '': - n_user.pop(k) - n_user[k] = int(dt_parse.parse(user[k]).timestamp() * 1000) + n_user[k] = int(dt_parse.parse(user[k]).timestamp() * 1000) if user[k] else None result.append(n_user) return result diff --git a/tests/data/lastpass/users.py b/tests/data/lastpass/users.py index a6217227a..6e30e9665 100644 --- a/tests/data/lastpass/users.py +++ b/tests/data/lastpass/users.py @@ -6,7 +6,7 @@ "mpstrength": "100", "created": "2022-08-31 04:45:15", "last_pw_change": "2022-08-31 06:03:16", - "last_login": "2023-01-12 09:43:34", + "last_login": "", "neverloggedin": False, "disabled": False, "admin": True, From 151f1bd3410471ae32cd471e4faf974e14d6ceb6 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Wed, 1 Mar 2023 12:53:51 +0100 Subject: [PATCH 08/18] add tenant object and migrate to new architecture --- .../jobs/analysis/lastpass_human_link.json | 13 ---- .../jobs/cleanup/lastpass_import_cleanup.json | 10 +++- cartography/intel/lastpass/__init__.py | 4 +- cartography/intel/lastpass/schema.py | 34 ----------- cartography/intel/lastpass/users.py | 31 ++++++---- cartography/models/lastpass/__init__.py | 0 cartography/models/lastpass/tenant.py | 40 +++++++++++++ cartography/models/lastpass/user.py | 59 +++++++++++++++++++ .../cartography/intel/lastpass/test_users.py | 7 ++- 9 files changed, 135 insertions(+), 63 deletions(-) delete mode 100644 cartography/data/jobs/analysis/lastpass_human_link.json delete mode 100644 cartography/intel/lastpass/schema.py create mode 100644 cartography/models/lastpass/__init__.py create mode 100644 cartography/models/lastpass/tenant.py create mode 100644 cartography/models/lastpass/user.py diff --git a/cartography/data/jobs/analysis/lastpass_human_link.json b/cartography/data/jobs/analysis/lastpass_human_link.json deleted file mode 100644 index 5b510a727..000000000 --- a/cartography/data/jobs/analysis/lastpass_human_link.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "statements": [ - { - "query": "MATCH (human:Human), (user:LastpassUser) WHERE human.email = user.email MERGE (human)-[r:IDENTITY_LASTPASS]->(user) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG", - "iterative": false - }, - { - "query": "MATCH (:Human)-[r:IDENTITY_LASTPASS]->(:LastpassUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DETACH DELETE (r) return COUNT(*) as TotalCompleted", - "iterative": true, - "iterationsize": 100 - }], - "name": "Lastpass user map to Human" -} diff --git a/cartography/data/jobs/cleanup/lastpass_import_cleanup.json b/cartography/data/jobs/cleanup/lastpass_import_cleanup.json index 4a2bc32c9..7d56293cf 100644 --- a/cartography/data/jobs/cleanup/lastpass_import_cleanup.json +++ b/cartography/data/jobs/cleanup/lastpass_import_cleanup.json @@ -3,6 +3,12 @@ "query": "MATCH (n:LastpassUser) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", "iterative": true, "iterationsize": 100 - }], - "name": "cleanup LastpassUser" + }, + { + "query": "MATCH (n:LastpassTenant) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100 + } +], + "name": "cleanup Lastpass" } diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index 5dcb7ce91..fc973fd44 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -28,13 +28,13 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No common_job_parameters = { "UPDATE_TAG": config.update_tag, + "LASTPASS_CID": config.lastpass_cid, } cartography.intel.lastpass.users.sync( neo4j_session, - config.update_tag, - config.lastpass_cid, config.lastpass_provhash, + common_job_parameters, ) run_cleanup_job( diff --git a/cartography/intel/lastpass/schema.py b/cartography/intel/lastpass/schema.py deleted file mode 100644 index 4bd6436f2..000000000 --- a/cartography/intel/lastpass/schema.py +++ /dev/null @@ -1,34 +0,0 @@ -from dataclasses import dataclass - -from cartography.graph.model import CartographyNodeProperties -from cartography.graph.model import CartographyNodeSchema -from cartography.graph.model import PropertyRef - - -@dataclass(frozen=True) -class LastpassUserNodeProperties(CartographyNodeProperties): - id: PropertyRef = PropertyRef('id') - lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) - name: PropertyRef = PropertyRef('fullname') - email: PropertyRef = PropertyRef('username') - created: PropertyRef = PropertyRef('created') - last_pw_change: PropertyRef = PropertyRef('last_pw_change') - last_login: PropertyRef = PropertyRef('last_login') - neverloggedin: PropertyRef = PropertyRef('neverloggedin') - disabled: PropertyRef = PropertyRef('disabled') - admin: PropertyRef = PropertyRef('admin') - totalscore: PropertyRef = PropertyRef('totalscore') - mpstrength: PropertyRef = PropertyRef('mpstrength') - sites: PropertyRef = PropertyRef('sites') - notes: PropertyRef = PropertyRef('notes') - formfills: PropertyRef = PropertyRef('formfills') - applications: PropertyRef = PropertyRef('applications') - attachments: PropertyRef = PropertyRef('attachments') - password_reset_required: PropertyRef = PropertyRef('password_reset_required') - multifactor: PropertyRef = PropertyRef('multifactor') - - -@dataclass(frozen=True) -class LastpassUserSchema(CartographyNodeSchema): - label: str = 'LastpassUser' - properties: LastpassUserNodeProperties = LastpassUserNodeProperties() diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 16ce628e0..4fa0271f6 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -9,7 +9,8 @@ from cartography.client.core.tx import load_graph_data from cartography.graph.querybuilder import build_ingestion_query -from cartography.intel.lastpass.schema import LastpassUserSchema +from cartography.models.lastpass.tenant import LastpassTenantSchema +from cartography.models.lastpass.user import LastpassUserSchema from cartography.util import timeit logger = logging.getLogger(__name__) @@ -20,19 +21,18 @@ @timeit def sync( neo4j_session: neo4j.Session, - update_tag: int, - lastpass_cid: str, lastpass_provhash: str, + common_job_parameters: Dict[str, Any], ) -> None: - users = get(lastpass_cid, lastpass_provhash) + users = get(lastpass_provhash, common_job_parameters) formated_users = transform(users) - load(neo4j_session, formated_users, update_tag) + load(neo4j_session, formated_users, common_job_parameters) @timeit -def get(lastpass_cid: str, lastpass_provhash: str) -> Dict[str, Any]: +def get(lastpass_provhash: str, common_job_parameters: Dict[str, Any]) -> Dict[str, Any]: payload = { - 'cid': lastpass_cid, + 'cid': common_job_parameters['LASTPASS_CID'], 'provhash': lastpass_provhash, 'cmd': 'getuserdata', 'data': None, @@ -58,14 +58,23 @@ def transform(api_result: dict) -> List[Dict]: def load( neo4j_session: neo4j.Session, data: List[Dict], - update_tag: int, + common_job_parameters: Dict[str, Any], ) -> None: - ingestion_query = build_ingestion_query(LastpassUserSchema()) + user_query = build_ingestion_query(LastpassUserSchema()) + tenant_query = build_ingestion_query(LastpassTenantSchema()) load_graph_data( neo4j_session, - ingestion_query, + user_query, data, - lastupdated=update_tag, + lastupdated=common_job_parameters['UPDATE_TAG'], + tenant_id=common_job_parameters['LASTPASS_CID'], + ) + + load_graph_data( + neo4j_session, + tenant_query, + [{'id': common_job_parameters['LASTPASS_CID']}], + lastupdated=common_job_parameters['UPDATE_TAG'], ) diff --git a/cartography/models/lastpass/__init__.py b/cartography/models/lastpass/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cartography/models/lastpass/tenant.py b/cartography/models/lastpass/tenant.py new file mode 100644 index 000000000..a0122bb0f --- /dev/null +++ b/cartography/models/lastpass/tenant.py @@ -0,0 +1,40 @@ +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 LastpassTenantNodeProperties(CartographyNodeProperties): + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +class TenantToUserRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +# (:LastpassTenant)<-[:RESOURCE]-(:LastpassUser) +class LastpassTenantToUserRel(CartographyRelSchema): + target_node_label: str = 'LastpassUser' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('tenant_id')}, + ) + direction: LinkDirection = LinkDirection.INWARD + rel_label: str = "RESOURCE" + properties: TenantToUserRelProperties = TenantToUserRelProperties() + + +@dataclass(frozen=True) +class LastpassTenantSchema(CartographyNodeSchema): + label: str = 'LastpassTenant' + properties: LastpassTenantNodeProperties = LastpassTenantNodeProperties() + sub_resource_relationship: LastpassTenantToUserRel = LastpassTenantToUserRel() diff --git a/cartography/models/lastpass/user.py b/cartography/models/lastpass/user.py new file mode 100644 index 000000000..ed2be55a2 --- /dev/null +++ b/cartography/models/lastpass/user.py @@ -0,0 +1,59 @@ +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 LastpassUserNodeProperties(CartographyNodeProperties): + id: PropertyRef = PropertyRef('id') + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + tenant_id: PropertyRef = PropertyRef('tenant_id', set_in_kwargs=True) + name: PropertyRef = PropertyRef('fullname') + email: PropertyRef = PropertyRef('username') + created: PropertyRef = PropertyRef('created') + last_pw_change: PropertyRef = PropertyRef('last_pw_change') + last_login: PropertyRef = PropertyRef('last_login') + neverloggedin: PropertyRef = PropertyRef('neverloggedin') + disabled: PropertyRef = PropertyRef('disabled') + admin: PropertyRef = PropertyRef('admin') + totalscore: PropertyRef = PropertyRef('totalscore') + mpstrength: PropertyRef = PropertyRef('mpstrength') + sites: PropertyRef = PropertyRef('sites') + notes: PropertyRef = PropertyRef('notes') + formfills: PropertyRef = PropertyRef('formfills') + applications: PropertyRef = PropertyRef('applications') + attachments: PropertyRef = PropertyRef('attachments') + password_reset_required: PropertyRef = PropertyRef('password_reset_required') + multifactor: PropertyRef = PropertyRef('multifactor') + + +@dataclass(frozen=True) +class HumanToUserRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +# (:LastpassUser)<-[:IDENTITY_LASTPASS]-(:Human) +class HumanToUserRel(CartographyRelSchema): + target_node_label: str = 'Human' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'email': PropertyRef('email')}, + ) + direction: LinkDirection = LinkDirection.INWARD + rel_label: str = "IDENTITY_LASTPASS" + properties: HumanToUserRelProperties = HumanToUserRelProperties() + + +@dataclass(frozen=True) +class LastpassUserSchema(CartographyNodeSchema): + label: str = 'LastpassUser' + properties: LastpassUserNodeProperties = LastpassUserNodeProperties() + other_relationships: OtherRelationships = OtherRelationships(rels=[HumanToUserRel()]) diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py index 9350b3dc2..9f39bf27f 100644 --- a/tests/integration/cartography/intel/lastpass/test_users.py +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -9,10 +9,15 @@ def test_load_lastpass_users(neo4j_session): data = tests.data.lastpass.users.LASTPASS_USERS formatted_data = cartography.intel.lastpass.users.transform(data) + common_job_parameters = { + "UPDATE_TAG": TEST_UPDATE_TAG, + "LASTPASS_CID": '1234', + } + cartography.intel.lastpass.users.load( neo4j_session, formatted_data, - TEST_UPDATE_TAG, + common_job_parameters, ) # Ensure users got loaded From 519e0d7c024853010d39641866e19ebf4ea676e3 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau <113923302+resilience-jychp@users.noreply.github.com> Date: Wed, 1 Mar 2023 13:48:05 +0100 Subject: [PATCH 09/18] Update tenant.py --- cartography/models/lastpass/tenant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cartography/models/lastpass/tenant.py b/cartography/models/lastpass/tenant.py index a0122bb0f..9a6da6895 100644 --- a/cartography/models/lastpass/tenant.py +++ b/cartography/models/lastpass/tenant.py @@ -26,7 +26,7 @@ class TenantToUserRelProperties(CartographyRelProperties): class LastpassTenantToUserRel(CartographyRelSchema): target_node_label: str = 'LastpassUser' target_node_matcher: TargetNodeMatcher = make_target_node_matcher( - {'id': PropertyRef('tenant_id')}, + {'tenant_id': PropertyRef('id')}, ) direction: LinkDirection = LinkDirection.INWARD rel_label: str = "RESOURCE" From 29f430ed34844f45166b72eba46b8a18f17c11ca Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 31 Mar 2023 14:57:47 +0200 Subject: [PATCH 10/18] rework to be compliant with new datamodel --- cartography/data/indexes.cypher | 3 --- .../jobs/cleanup/lastpass_import_cleanup.json | 14 ----------- cartography/intel/lastpass/users.py | 10 ++++---- cartography/models/lastpass/tenant.py | 23 ------------------- cartography/models/lastpass/user.py | 21 +++++++++++++++-- 5 files changed, 24 insertions(+), 47 deletions(-) delete mode 100644 cartography/data/jobs/cleanup/lastpass_import_cleanup.json diff --git a/cartography/data/indexes.cypher b/cartography/data/indexes.cypher index b77ffc272..6029d2460 100644 --- a/cartography/data/indexes.cypher +++ b/cartography/data/indexes.cypher @@ -451,6 +451,3 @@ CREATE INDEX IF NOT EXISTS FOR (n:KubernetesSecret) ON (n.lastupdated); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.id); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.name); CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.lastupdated); -CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.id); -CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.email); -CREATE INDEX IF NOT EXISTS FOR (n:LastpassUser) ON (n.lastupdated); diff --git a/cartography/data/jobs/cleanup/lastpass_import_cleanup.json b/cartography/data/jobs/cleanup/lastpass_import_cleanup.json deleted file mode 100644 index 7d56293cf..000000000 --- a/cartography/data/jobs/cleanup/lastpass_import_cleanup.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "statements": [{ - "query": "MATCH (n:LastpassUser) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", - "iterative": true, - "iterationsize": 100 - }, - { - "query": "MATCH (n:LastpassTenant) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", - "iterative": true, - "iterationsize": 100 - } -], - "name": "cleanup Lastpass" -} diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 4fa0271f6..4e18e4ba3 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -66,15 +66,15 @@ def load( load_graph_data( neo4j_session, - user_query, - data, + tenant_query, + [{'id': common_job_parameters['LASTPASS_CID']}], lastupdated=common_job_parameters['UPDATE_TAG'], - tenant_id=common_job_parameters['LASTPASS_CID'], ) load_graph_data( neo4j_session, - tenant_query, - [{'id': common_job_parameters['LASTPASS_CID']}], + user_query, + data, lastupdated=common_job_parameters['UPDATE_TAG'], + tenant_id=common_job_parameters['LASTPASS_CID'], ) diff --git a/cartography/models/lastpass/tenant.py b/cartography/models/lastpass/tenant.py index 9a6da6895..65e80c3ce 100644 --- a/cartography/models/lastpass/tenant.py +++ b/cartography/models/lastpass/tenant.py @@ -3,11 +3,6 @@ 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) @@ -16,25 +11,7 @@ class LastpassTenantNodeProperties(CartographyNodeProperties): lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) -@dataclass(frozen=True) -class TenantToUserRelProperties(CartographyRelProperties): - lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) - - -@dataclass(frozen=True) -# (:LastpassTenant)<-[:RESOURCE]-(:LastpassUser) -class LastpassTenantToUserRel(CartographyRelSchema): - target_node_label: str = 'LastpassUser' - target_node_matcher: TargetNodeMatcher = make_target_node_matcher( - {'tenant_id': PropertyRef('id')}, - ) - direction: LinkDirection = LinkDirection.INWARD - rel_label: str = "RESOURCE" - properties: TenantToUserRelProperties = TenantToUserRelProperties() - - @dataclass(frozen=True) class LastpassTenantSchema(CartographyNodeSchema): label: str = 'LastpassTenant' properties: LastpassTenantNodeProperties = LastpassTenantNodeProperties() - sub_resource_relationship: LastpassTenantToUserRel = LastpassTenantToUserRel() diff --git a/cartography/models/lastpass/user.py b/cartography/models/lastpass/user.py index ed2be55a2..f7b16efe8 100644 --- a/cartography/models/lastpass/user.py +++ b/cartography/models/lastpass/user.py @@ -15,9 +15,8 @@ class LastpassUserNodeProperties(CartographyNodeProperties): id: PropertyRef = PropertyRef('id') lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) - tenant_id: PropertyRef = PropertyRef('tenant_id', set_in_kwargs=True) name: PropertyRef = PropertyRef('fullname') - email: PropertyRef = PropertyRef('username') + email: PropertyRef = PropertyRef('username', extra_index=True) created: PropertyRef = PropertyRef('created') last_pw_change: PropertyRef = PropertyRef('last_pw_change') last_login: PropertyRef = PropertyRef('last_login') @@ -52,8 +51,26 @@ class HumanToUserRel(CartographyRelSchema): properties: HumanToUserRelProperties = HumanToUserRelProperties() +@dataclass(frozen=True) +class TenantToUserRelProperties(CartographyRelProperties): + lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) + + +@dataclass(frozen=True) +# (:LastpassTenant)<-[:RESOURCE]-(:LastpassUser) +class LastpassTenantToUserRel(CartographyRelSchema): + target_node_label: str = 'LastpassTenant' + target_node_matcher: TargetNodeMatcher = make_target_node_matcher( + {'id': PropertyRef('tenant_id', set_in_kwargs=True)}, + ) + direction: LinkDirection = LinkDirection.OUTWARD + rel_label: str = "RESOURCE" + properties: TenantToUserRelProperties = TenantToUserRelProperties() + + @dataclass(frozen=True) class LastpassUserSchema(CartographyNodeSchema): label: str = 'LastpassUser' properties: LastpassUserNodeProperties = LastpassUserNodeProperties() other_relationships: OtherRelationships = OtherRelationships(rels=[HumanToUserRel()]) + sub_resource_relationship: LastpassTenantToUserRel = LastpassTenantToUserRel() From a77e3ddfab5b1efd2ab763d7e0165ad324b7fd8c Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Mon, 3 Apr 2023 15:41:28 +0200 Subject: [PATCH 11/18] remove cleanup job --- cartography/intel/lastpass/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index fc973fd44..1fb6854a0 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -4,7 +4,6 @@ import cartography.intel.lastpass.users from cartography.config import Config -from cartography.util import run_cleanup_job from cartography.util import timeit logger = logging.getLogger(__name__) @@ -36,9 +35,3 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No config.lastpass_provhash, common_job_parameters, ) - - run_cleanup_job( - "lastpass_import_cleanup.json", - neo4j_session, - common_job_parameters, - ) From 0b46c3acb4cb8d251509d2799deac0b41af5d05a Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 7 Apr 2023 09:44:12 +0200 Subject: [PATCH 12/18] apply some review fix --- cartography/intel/lastpass/users.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 4e18e4ba3..a7cca4c56 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -7,8 +7,8 @@ from dateutil import parser as dt_parse from requests import Session -from cartography.client.core.tx import load_graph_data -from cartography.graph.querybuilder import build_ingestion_query +from cartography.client.core.tx import load +from cartography.graph.job import GraphJob from cartography.models.lastpass.tenant import LastpassTenantSchema from cartography.models.lastpass.user import LastpassUserSchema from cartography.util import timeit @@ -26,7 +26,8 @@ def sync( ) -> None: users = get(lastpass_provhash, common_job_parameters) formated_users = transform(users) - load(neo4j_session, formated_users, common_job_parameters) + load_users(neo4j_session, formated_users, common_job_parameters) + cleanup(neo4j_session, common_job_parameters) @timeit @@ -44,8 +45,8 @@ def get(lastpass_provhash: str, common_job_parameters: Dict[str, Any]) -> Dict[s @timeit -def transform(api_result: dict) -> List[Dict]: - result: List[dict] = [] +def transform(api_result: Dict[str, Any]) -> List[Dict[str, Any]]: + result: List[Dict[str, Any]] = [] for uid, user in api_result['Users'].items(): n_user = user.copy() n_user['id'] = int(uid) @@ -55,26 +56,27 @@ def transform(api_result: dict) -> List[Dict]: return result -def load( +def load_users( neo4j_session: neo4j.Session, data: List[Dict], common_job_parameters: Dict[str, Any], ) -> None: - user_query = build_ingestion_query(LastpassUserSchema()) - tenant_query = build_ingestion_query(LastpassTenantSchema()) - - load_graph_data( + load( neo4j_session, - tenant_query, + LastpassTenantSchema(), [{'id': common_job_parameters['LASTPASS_CID']}], lastupdated=common_job_parameters['UPDATE_TAG'], ) - load_graph_data( + load( neo4j_session, - user_query, + LastpassUserSchema(), data, lastupdated=common_job_parameters['UPDATE_TAG'], tenant_id=common_job_parameters['LASTPASS_CID'], ) + + +def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None: + GraphJob.from_node_schema(LastpassUserSchema(), common_job_parameters).run(neo4j_session) From 9433ae0162dca4940b938c0a07db43d63772789c Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 7 Apr 2023 09:50:33 +0200 Subject: [PATCH 13/18] fix integration test --- tests/integration/cartography/intel/lastpass/test_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py index 9f39bf27f..73d30228a 100644 --- a/tests/integration/cartography/intel/lastpass/test_users.py +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -14,7 +14,7 @@ def test_load_lastpass_users(neo4j_session): "LASTPASS_CID": '1234', } - cartography.intel.lastpass.users.load( + cartography.intel.lastpass.users.load_users( neo4j_session, formatted_data, common_job_parameters, From 2151b8f5d0170155eaa6489b06e3bd7c8c2f835e Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Tue, 11 Apr 2023 13:59:52 +0200 Subject: [PATCH 14/18] common_parameters fix for cleanup job --- cartography/intel/lastpass/__init__.py | 2 +- cartography/intel/lastpass/users.py | 6 +++--- tests/integration/cartography/intel/lastpass/test_users.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index 1fb6854a0..1951a4957 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -27,7 +27,7 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No common_job_parameters = { "UPDATE_TAG": config.update_tag, - "LASTPASS_CID": config.lastpass_cid, + "tenant_id": config.lastpass_cid, } cartography.intel.lastpass.users.sync( diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index a7cca4c56..c053de07a 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -33,7 +33,7 @@ def sync( @timeit def get(lastpass_provhash: str, common_job_parameters: Dict[str, Any]) -> Dict[str, Any]: payload = { - 'cid': common_job_parameters['LASTPASS_CID'], + 'cid': common_job_parameters['tenant_id'], 'provhash': lastpass_provhash, 'cmd': 'getuserdata', 'data': None, @@ -65,7 +65,7 @@ def load_users( load( neo4j_session, LastpassTenantSchema(), - [{'id': common_job_parameters['LASTPASS_CID']}], + [{'id': common_job_parameters['tenant_id']}], lastupdated=common_job_parameters['UPDATE_TAG'], ) @@ -74,7 +74,7 @@ def load_users( LastpassUserSchema(), data, lastupdated=common_job_parameters['UPDATE_TAG'], - tenant_id=common_job_parameters['LASTPASS_CID'], + tenant_id=common_job_parameters['tenant_id'], ) diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py index 73d30228a..3468dba47 100644 --- a/tests/integration/cartography/intel/lastpass/test_users.py +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -11,7 +11,7 @@ def test_load_lastpass_users(neo4j_session): formatted_data = cartography.intel.lastpass.users.transform(data) common_job_parameters = { "UPDATE_TAG": TEST_UPDATE_TAG, - "LASTPASS_CID": '1234', + "tenant_id": '1234', } cartography.intel.lastpass.users.load_users( From b4058f0d978fcd5d10cc37839f0d7414b0a26604 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 14 Apr 2023 10:55:53 +0200 Subject: [PATCH 15/18] improve intel module --- cartography/intel/lastpass/__init__.py | 2 ++ cartography/intel/lastpass/users.py | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index 1951a4957..aeac73bf1 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -33,5 +33,7 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No cartography.intel.lastpass.users.sync( neo4j_session, config.lastpass_provhash, + int(config.lastpass_cid), + config.lastpass_cid, common_job_parameters, ) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index c053de07a..216878b18 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -22,18 +22,20 @@ def sync( neo4j_session: neo4j.Session, lastpass_provhash: str, + tenant_id: int, + update_tag: int, common_job_parameters: Dict[str, Any], ) -> None: - users = get(lastpass_provhash, common_job_parameters) + users = get(lastpass_provhash, tenant_id) formated_users = transform(users) - load_users(neo4j_session, formated_users, common_job_parameters) + load_users(neo4j_session, formated_users, tenant_id, update_tag) cleanup(neo4j_session, common_job_parameters) @timeit -def get(lastpass_provhash: str, common_job_parameters: Dict[str, Any]) -> Dict[str, Any]: +def get(lastpass_provhash: str, tenant_id: int) -> Dict[str, Any]: payload = { - 'cid': common_job_parameters['tenant_id'], + 'cid': tenant_id, 'provhash': lastpass_provhash, 'cmd': 'getuserdata', 'data': None, @@ -58,23 +60,24 @@ def transform(api_result: Dict[str, Any]) -> List[Dict[str, Any]]: def load_users( neo4j_session: neo4j.Session, - data: List[Dict], - common_job_parameters: Dict[str, Any], + data: List[Dict[str, Any]], + tenant_id: int, + update_tag: int, ) -> None: load( neo4j_session, LastpassTenantSchema(), - [{'id': common_job_parameters['tenant_id']}], - lastupdated=common_job_parameters['UPDATE_TAG'], + [{'id': tenant_id}], + lastupdated=update_tag, ) load( neo4j_session, LastpassUserSchema(), data, - lastupdated=common_job_parameters['UPDATE_TAG'], - tenant_id=common_job_parameters['tenant_id'], + lastupdated=update_tag, + tenant_id=tenant_id, ) From 21ab0f726caf3f65d3fdef0b62f70b9dc18c44f3 Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau Date: Fri, 14 Apr 2023 15:44:20 +0200 Subject: [PATCH 16/18] rework testing --- cartography/intel/lastpass/__init__.py | 4 +- cartography/intel/lastpass/users.py | 2 +- cartography/models/lastpass/user.py | 4 +- docker-compose.yml | 2 +- tests/data/lastpass/users.py | 20 ++++ .../cartography/intel/lastpass/test_users.py | 100 ++++++++++++++---- 6 files changed, 103 insertions(+), 29 deletions(-) diff --git a/cartography/intel/lastpass/__init__.py b/cartography/intel/lastpass/__init__.py index aeac73bf1..200356705 100644 --- a/cartography/intel/lastpass/__init__.py +++ b/cartography/intel/lastpass/__init__.py @@ -27,13 +27,13 @@ def start_lastpass_ingestion(neo4j_session: neo4j.Session, config: Config) -> No common_job_parameters = { "UPDATE_TAG": config.update_tag, - "tenant_id": config.lastpass_cid, + "TENANT_ID": config.lastpass_cid, } cartography.intel.lastpass.users.sync( neo4j_session, config.lastpass_provhash, int(config.lastpass_cid), - config.lastpass_cid, + config.update_tag, common_job_parameters, ) diff --git a/cartography/intel/lastpass/users.py b/cartography/intel/lastpass/users.py index 216878b18..df5f09825 100644 --- a/cartography/intel/lastpass/users.py +++ b/cartography/intel/lastpass/users.py @@ -77,7 +77,7 @@ def load_users( LastpassUserSchema(), data, lastupdated=update_tag, - tenant_id=tenant_id, + TENANT_ID=tenant_id, ) diff --git a/cartography/models/lastpass/user.py b/cartography/models/lastpass/user.py index f7b16efe8..c01a11b71 100644 --- a/cartography/models/lastpass/user.py +++ b/cartography/models/lastpass/user.py @@ -44,7 +44,7 @@ class HumanToUserRelProperties(CartographyRelProperties): class HumanToUserRel(CartographyRelSchema): target_node_label: str = 'Human' target_node_matcher: TargetNodeMatcher = make_target_node_matcher( - {'email': PropertyRef('email')}, + {'email': PropertyRef('username')}, ) direction: LinkDirection = LinkDirection.INWARD rel_label: str = "IDENTITY_LASTPASS" @@ -61,7 +61,7 @@ class TenantToUserRelProperties(CartographyRelProperties): class LastpassTenantToUserRel(CartographyRelSchema): target_node_label: str = 'LastpassTenant' target_node_matcher: TargetNodeMatcher = make_target_node_matcher( - {'id': PropertyRef('tenant_id', set_in_kwargs=True)}, + {'id': PropertyRef('TENANT_ID', set_in_kwargs=True)}, ) direction: LinkDirection = LinkDirection.OUTWARD rel_label: str = "RESOURCE" diff --git a/docker-compose.yml b/docker-compose.yml index da2a21f7e..ce6696384 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: timeout: 10s retries: 10 cartography: - image: ghcr.io/lyft/cartography + image: lyft/cartography # EXAMPLE: Our ENTRYPOINT is cartography, running specific command to sync AWS # command: ["-v", "--neo4j-uri=bolt://neo4j:7687", "--aws-sync-all-profiles"] user: cartography diff --git a/tests/data/lastpass/users.py b/tests/data/lastpass/users.py index 6e30e9665..c18100ccc 100644 --- a/tests/data/lastpass/users.py +++ b/tests/data/lastpass/users.py @@ -20,5 +20,25 @@ "attachments": 0, "password_reset_required": False, }, + 234567: { + "username": "jane.smith@domain.tld", + "fullname": "Jane Smith", + "mpstrength": "60", + "created": "2023-03-18 12:42:03", + "last_pw_change": "2023-03-18 12:42:03", + "last_login": "2023-03-18 12:42:03", + "neverloggedin": False, + "disabled": True, + "admin": False, + "totalscore": "16.4", + "multifactor": "lastpassauth", + "duousername": "jane.smith@domain.tld", + "sites": 12, + "notes": 0, + "formfills": 0, + "applications": 0, + "attachments": 2, + "password_reset_required": False, + }, }, } diff --git a/tests/integration/cartography/intel/lastpass/test_users.py b/tests/integration/cartography/intel/lastpass/test_users.py index 3468dba47..e81898eef 100644 --- a/tests/integration/cartography/intel/lastpass/test_users.py +++ b/tests/integration/cartography/intel/lastpass/test_users.py @@ -1,38 +1,92 @@ +from unittest.mock import patch + import cartography.intel.lastpass.users import tests.data.lastpass.users - +from tests.integration.util import check_nodes +from tests.integration.util import check_rels TEST_UPDATE_TAG = 123456789 +TEST_TENANT_ID = 11223344 -def test_load_lastpass_users(neo4j_session): +@patch.object(cartography.intel.lastpass.users, 'get', return_value=tests.data.lastpass.users.LASTPASS_USERS) +def test_load_lastpass_users(mock_api, neo4j_session): + """ + Ensure that users actually get loaded + """ - data = tests.data.lastpass.users.LASTPASS_USERS - formatted_data = cartography.intel.lastpass.users.transform(data) - common_job_parameters = { - "UPDATE_TAG": TEST_UPDATE_TAG, - "tenant_id": '1234', - } + # Arrange + # LastPass intel only link users to existing Humans (created by other module like Gsuite) + # We have to create mock humans to tests rels are well created by Lastpass module + query = """ + UNWIND $UserData as user - cartography.intel.lastpass.users.load_users( - neo4j_session, - formatted_data, - common_job_parameters, + MERGE (h:Human{id: user}) + ON CREATE SET h.firstseen = timestamp() + SET h.email = user, + h.email = user, + h.lastupdated = $UpdateTag + """ + data = [] + for v in tests.data.lastpass.users.LASTPASS_USERS['Users'].values(): + data.append(v['username']) + neo4j_session.run( + query, + UserData=data, + UpdateTag=TEST_UPDATE_TAG, ) - # Ensure users got loaded - nodes = neo4j_session.run( - """ - MATCH (e:LastpassUser) RETURN e.id, e.email; - """, + # Act + cartography.intel.lastpass.users.sync( + neo4j_session, + 'fakeProvHash', + TEST_TENANT_ID, + TEST_UPDATE_TAG, + {"UPDATE_TAG": TEST_UPDATE_TAG, "TENANT_ID": TEST_TENANT_ID}, ) + + # Assert Human exists + expected_nodes = { + ('john.doe@domain.tld', 'john.doe@domain.tld'), + ('jane.smith@domain.tld', 'jane.smith@domain.tld'), + } + assert check_nodes(neo4j_session, 'Human', ['id', 'email']) == expected_nodes + + # Assert Tenant exists + expected_nodes = { + (TEST_TENANT_ID,), + } + assert check_nodes(neo4j_session, 'LastpassTenant', ['id']) == expected_nodes + + # Assert Users exists expected_nodes = { (123456, 'john.doe@domain.tld'), + (234567, 'jane.smith@domain.tld'), } - actual_nodes = { - ( - n['e.id'], - n['e.email'], - ) for n in nodes + assert check_nodes(neo4j_session, 'LastpassUser', ['id', 'email']) == expected_nodes + + # Assert Users are connected with Tenant + expected_rels = { + (123456, TEST_TENANT_ID), + (234567, TEST_TENANT_ID), } - assert actual_nodes == expected_nodes + assert check_rels( + neo4j_session, + 'LastpassUser', 'id', + 'LastpassTenant', 'id', + 'RESOURCE', + rel_direction_right=True, + ) == expected_rels + + # Assert Users are connected with Humans + expected_rels = { + (123456, 'john.doe@domain.tld'), + (234567, 'jane.smith@domain.tld'), + } + assert check_rels( + neo4j_session, + 'LastpassUser', 'id', + 'Human', 'email', + 'IDENTITY_LASTPASS', + rel_direction_right=False, + ) == expected_rels From 4f63fc979d8bd867df0011229c2a0d3b2e46027e Mon Sep 17 00:00:00 2001 From: Jeremy Chapeau <113923302+resilience-jychp@users.noreply.github.com> Date: Sat, 15 Apr 2023 07:24:02 +0200 Subject: [PATCH 17/18] Update docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce6696384..da2a21f7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: timeout: 10s retries: 10 cartography: - image: lyft/cartography + image: ghcr.io/lyft/cartography # EXAMPLE: Our ENTRYPOINT is cartography, running specific command to sync AWS # command: ["-v", "--neo4j-uri=bolt://neo4j:7687", "--aws-sync-all-profiles"] user: cartography From 11b53d5c79eb073c103109ab9edb987be6057eb9 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave <32398091+ramonpetgrave64@users.noreply.github.com> Date: Mon, 17 Apr 2023 15:25:54 -0400 Subject: [PATCH 18/18] specify as LastPassUser --- cartography/models/lastpass/user.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cartography/models/lastpass/user.py b/cartography/models/lastpass/user.py index c01a11b71..20421c749 100644 --- a/cartography/models/lastpass/user.py +++ b/cartography/models/lastpass/user.py @@ -35,24 +35,24 @@ class LastpassUserNodeProperties(CartographyNodeProperties): @dataclass(frozen=True) -class HumanToUserRelProperties(CartographyRelProperties): +class LastpassUserToHumanRelProperties(CartographyRelProperties): lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) @dataclass(frozen=True) # (:LastpassUser)<-[:IDENTITY_LASTPASS]-(:Human) -class HumanToUserRel(CartographyRelSchema): +class LastpassHumanToUserRel(CartographyRelSchema): target_node_label: str = 'Human' target_node_matcher: TargetNodeMatcher = make_target_node_matcher( {'email': PropertyRef('username')}, ) direction: LinkDirection = LinkDirection.INWARD rel_label: str = "IDENTITY_LASTPASS" - properties: HumanToUserRelProperties = HumanToUserRelProperties() + properties: LastpassUserToHumanRelProperties = LastpassUserToHumanRelProperties() @dataclass(frozen=True) -class TenantToUserRelProperties(CartographyRelProperties): +class LastpassTenantToLastpassUserRelProperties(CartographyRelProperties): lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True) @@ -65,12 +65,12 @@ class LastpassTenantToUserRel(CartographyRelSchema): ) direction: LinkDirection = LinkDirection.OUTWARD rel_label: str = "RESOURCE" - properties: TenantToUserRelProperties = TenantToUserRelProperties() + properties: LastpassTenantToLastpassUserRelProperties = LastpassTenantToLastpassUserRelProperties() @dataclass(frozen=True) class LastpassUserSchema(CartographyNodeSchema): label: str = 'LastpassUser' properties: LastpassUserNodeProperties = LastpassUserNodeProperties() - other_relationships: OtherRelationships = OtherRelationships(rels=[HumanToUserRel()]) + other_relationships: OtherRelationships = OtherRelationships(rels=[LastpassHumanToUserRel()]) sub_resource_relationship: LastpassTenantToUserRel = LastpassTenantToUserRel()