Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lastpass source #1083

Merged
merged 23 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions cartography/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,22 @@ def _build_parser(self):
'The name of environment variable containing secrets for GSuite authentication.'
),
)
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',
jychp marked this conversation as resolved.
Show resolved Hide resolved
type=str,
default=None,
help=(
'The name of environment variable containing the Lastpass provhash for authentication.'
),
)
return parser

def main(self, argv: str) -> int:
Expand Down Expand Up @@ -556,6 +572,18 @@ def main(self, argv: str) -> int:
else:
config.github_config = 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)
Expand Down
8 changes: 8 additions & 0 deletions cartography/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class Config:
:param gsuite_auth_method: Auth method (delegated, oauth) used for Google Workspace. Optional.
:type gsuite_config: str
:param gsuite_config: Base64 encoded config object or config file path for Google Workspace. 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__(
Expand Down Expand Up @@ -130,6 +134,8 @@ def __init__(
crowdstrike_api_url=None,
gsuite_auth_method=None,
gsuite_config=None,
lastpass_cid=None,
lastpass_provhash=None,
):
self.neo4j_uri = neo4j_uri
self.neo4j_user = neo4j_user
Expand Down Expand Up @@ -172,3 +178,5 @@ def __init__(
self.crowdstrike_api_url = crowdstrike_api_url
self.gsuite_auth_method = gsuite_auth_method
self.gsuite_config = gsuite_config
self.lastpass_cid = lastpass_cid
self.lastpass_provhash = lastpass_provhash
3 changes: 3 additions & 0 deletions cartography/data/indexes.cypher
Original file line number Diff line number Diff line change
Expand Up @@ -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);
13 changes: 13 additions & 0 deletions cartography/data/jobs/analysis/lastpass_human_link.json
Original file line number Diff line number Diff line change
@@ -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",
jychp marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
8 changes: 8 additions & 0 deletions cartography/data/jobs/cleanup/lastpass_import_cleanup.json
Original file line number Diff line number Diff line change
@@ -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"
}
44 changes: 44 additions & 0 deletions cartography/intel/lastpass/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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,
)
34 changes: 34 additions & 0 deletions cartography/intel/lastpass/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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):
jychp marked this conversation as resolved.
Show resolved Hide resolved
label: str = 'LastpassUser'
properties: LastpassUserNodeProperties = LastpassUserNodeProperties()
jychp marked this conversation as resolved.
Show resolved Hide resolved
71 changes: 71 additions & 0 deletions cartography/intel/lastpass/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging
from typing import Any
from typing import Dict
from typing import List

import neo4j
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__)
# Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
_TIMEOUT = (60, 60)


@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[str, Any]:
payload = {
'cid': lastpass_cid,
'provhash': lastpass_provhash,
'cmd': 'getuserdata',
'data': None,
}
session = Session()
req = session.post('https://lastpass.com/enterpriseapi.php', data=payload, timeout=_TIMEOUT)
req.raise_for_status()
return req.json()


@timeit
def transform(api_result: dict) -> List[Dict]:
jychp marked this conversation as resolved.
Show resolved Hide resolved
result: List[dict] = []
jychp marked this conversation as resolved.
Show resolved Hide resolved
for uid, user in api_result['Users'].items():
n_user = user.copy()
n_user['id'] = int(uid)
for k in ('created', 'last_pw_change', 'last_login'):
n_user[k] = int(dt_parse.parse(user[k]).timestamp() * 1000) if user[k] else None
jychp marked this conversation as resolved.
Show resolved Hide resolved
result.append(n_user)
return result


def load(
jychp marked this conversation as resolved.
Show resolved Hide resolved
neo4j_session: neo4j.Session,
data: List[Dict],
jychp marked this conversation as resolved.
Show resolved Hide resolved
update_tag: int,
) -> None:

ingestion_query = build_ingestion_query(LastpassUserSchema())

load_graph_data(
neo4j_session,
ingestion_query,
data,
lastupdated=update_tag,
)
2 changes: 2 additions & 0 deletions cartography/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions docs/root/modules/lastpass/config.md
Original file line number Diff line number Diff line change
@@ -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)
jychp marked this conversation as resolved.
Show resolved Hide resolved
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.
12 changes: 12 additions & 0 deletions docs/root/modules/lastpass/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Lastpass
########

The lastpass module has the following coverage:

* Users

.. toctree::
:hidden:
:glob:

*
45 changes: 45 additions & 0 deletions docs/root/modules/lastpass/schema.md
Original file line number Diff line number Diff line change
@@ -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) |
Empty file added tests/data/lastpass/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/data/lastpass/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
LASTPASS_USERS = {
'Users': {
123456: {
"username": "[email protected]",
"fullname": "John Doe",
"mpstrength": "100",
"created": "2022-08-31 04:45:15",
"last_pw_change": "2022-08-31 06:03:16",
"last_login": "",
"neverloggedin": False,
"disabled": False,
"admin": True,
"totalscore": "83.2",
"multifactor": "lastpassauth",
"duousername": "[email protected]",
"sites": 25,
"notes": 1,
"formfills": 0,
"applications": 0,
"attachments": 0,
"password_reset_required": False,
},
},
}
Empty file.
Loading