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

[DC] Meraki Device Connector #290

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c001f5e
first commit
kuannie1 Aug 23, 2019
1d8a45b
meraki connector skeleton (needs custom updating)
kuannie1 Aug 23, 2019
b0e4801
rebase
alldoami Aug 27, 2019
b30174b
meraki logo
alldoami Aug 26, 2019
24f0664
changed secrets
alldoami Aug 26, 2019
92b01f0
a connector that runs. next: making more tables and double checking v…
kuannie1 Aug 26, 2019
179325b
create two tables, has numeric value empty string is not recognized e…
alldoami Aug 27, 2019
f509053
removed None
alldoami Aug 27, 2019
d777a21
deleted unnecessary functions and included db.py functions
kuannie1 Aug 27, 2019
706624a
debug
alldoami Aug 27, 2019
81f4a11
ingesting meraki client & device info and inserting into landing table
kuannie1 Aug 27, 2019
327ae3f
Update src/connectors/meraki.py
kuannie1 Aug 27, 2019
fd457c3
first commit
kuannie1 Aug 23, 2019
fcc2520
meraki connector skeleton (needs custom updating)
kuannie1 Aug 23, 2019
2a8c475
rebase
alldoami Aug 27, 2019
bb027a5
meraki logo
alldoami Aug 26, 2019
d0eb88a
changed secrets
alldoami Aug 26, 2019
a881d9b
a connector that runs. next: making more tables and double checking v…
kuannie1 Aug 26, 2019
e413652
create two tables, has numeric value empty string is not recognized e…
alldoami Aug 27, 2019
d7bb036
removed None
alldoami Aug 27, 2019
47859d9
deleted unnecessary functions and included db.py functions
kuannie1 Aug 27, 2019
db03993
debug
alldoami Aug 27, 2019
e7487e1
ingesting meraki client & device info and inserting into landing table
kuannie1 Aug 27, 2019
b154502
Update src/connectors/meraki.py
kuannie1 Aug 27, 2019
d39979e
Merge branch 'aku/meraki_connector' of github.com:chanzuckerberg/Snow…
kuannie1 Aug 27, 2019
f78dc8a
made some successful changes but pushing before I break anything else
kuannie1 Aug 27, 2019
4bf02cb
combined table variables
alldoami Aug 27, 2019
a0f99f4
fixed try except network
alldoami Aug 27, 2019
409b2d0
fixed syntax
alldoami Aug 27, 2019
f7d259d
deleting redundant pictures
kuannie1 Aug 27, 2019
801d6cc
Merge branch 'aku/meraki_connector' of github.com:chanzuckerberg/Snow…
kuannie1 Aug 27, 2019
64276bc
remaining changes
kuannie1 Aug 27, 2019
54ef870
changed whitelist input type
alldoami Aug 27, 2019
279338c
inserting clients in batches
kuannie1 Aug 27, 2019
fe2fb8f
batching clients
kuannie1 Aug 27, 2019
e7be0aa
resolved merge conflicts
kuannie1 Aug 28, 2019
f903a47
small changes
kuannie1 Aug 28, 2019
a9f25dd
more small changes
kuannie1 Aug 28, 2019
dc91f32
default value for get
alldoami Aug 30, 2019
eef66e7
fix merge
alldoami Aug 30, 2019
09bffa6
fix get to default to None
alldoami Aug 30, 2019
26d1589
unfinished meraki file
kuannie1 Sep 6, 2019
2ff4139
only 1 input table in ingest()
kuannie1 Sep 6, 2019
6260259
Revert "unfinished meraki file"
kuannie1 Sep 6, 2019
9d8b261
only 1 table in ingest()
kuannie1 Sep 6, 2019
af7c6c4
Update src/connectors/__init__.py
kuannie1 Sep 6, 2019
a386650
Eduardo's suggestions from code review
kuannie1 Sep 6, 2019
c020007
Apply suggestions from code review
kuannie1 Sep 6, 2019
75e301e
renaming meraki to meraki_devices and code review suggestions
kuannie1 Sep 6, 2019
a8f2f38
andrey's feedback
kuannie1 Sep 6, 2019
7ae202a
Delete cisco_umbrella.png
sfc-gh-afedorov Sep 7, 2019
a0fb70c
Merge branch 'v1.8.6' into aku/meraki_connector
sfc-gh-afedorov Sep 7, 2019
c09881b
Update __init__.py
sfc-gh-afedorov Sep 7, 2019
383b997
Update meraki_devices.py
sfc-gh-afedorov Sep 7, 2019
b466ccc
followed pep8 standards
kuannie1 Sep 9, 2019
b147f05
Merge branch 'v1.8.6' into aku/meraki_connector
kuannie1 Sep 9, 2019
158ccd8
meraki works with WebUI
kuannie1 Sep 9, 2019
78acd49
Update meraki_devices.py
sfc-gh-afedorov Sep 10, 2019
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
3 changes: 3 additions & 0 deletions src/connectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import tenable_settings
from . import crowdstrike_devices
from . import cisco_umbrella
from . import meraki_devices
from . import assetpanda
from . import nginx_log
from . import ldap_log
Expand All @@ -28,6 +29,7 @@
'azure_vm',
'github_webhooks_s3',
'gsuite_logs',
'meraki_devices',
'okta',
'osquery_log',
'tenable_settings',
Expand All @@ -49,6 +51,7 @@
'azure_vm': azure_vm,
'github_webhooks_s3': github_webhooks_s3,
'gsuite_logs': gsuite_logs,
'meraki_devices': meraki_devices,
'okta': okta,
'osquery_log': osquery_log,
'tenable_settings': tenable_settings,
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/assetpanda.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from datetime import datetime


PAGE_SIZE = 50
PAGE_SIZE = 1000

CONNECTION_OPTIONS = [
{
Expand Down
185 changes: 185 additions & 0 deletions src/connectors/meraki_devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Meraki Devices
Collect Meraki Device information using an API Token
"""

from runners.helpers import log
from runners.helpers import db
from runners.helpers.dbconfig import ROLE as SA_ROLE

from datetime import datetime

import requests
from urllib.error import HTTPError
from .utils import yaml_dump


PAGE_SIZE = 5

CONNECTION_OPTIONS = [
{
'name': 'api_token',
'title': "Meraki API Token",
'prompt': "Your Meraki API Token",
'type': 'str',
'secret': True,
'required': True,
},
{
'name': 'network_id_whitelist',
'title': "Meraki Network Ids Whitelist",
'prompt': "Whitelist of Network Ids",
'type': 'list',
'secret': False,
'required': True,
},
]

LANDING_TABLE_COLUMNS_CLIENT = [
('insert_id', 'NUMBER IDENTITY START 1 INCREMENT 1'),
('snapshot_at', 'TIMESTAMP_LTZ(9)'),
('raw', 'VARIANT'),
('id', 'VARCHAR(256)'),
('mac', 'VARCHAR(256)'),
('description', 'VARCHAR(256)'),
('mdns_name', 'VARCHAR(256)'),
('dhcp_hostname', 'VARCHAR(256)'),
('ip', 'VARCHAR(256)'),
('switchport', 'VARCHAR(256)'),
('vlan', 'INT'),
('usage_sent', 'INT'),
('usage_recv', 'INT'),
('serial', 'VARCHAR(256)'),
]

LANDING_TABLE_COLUMNS_DEVICE = [
('insert_id', 'NUMBER IDENTITY START 1 INCREMENT 1'),
('snapshot_at', 'TIMESTAMP_LTZ(9)'),
('raw', 'VARIANT'),
('serial', 'VARCHAR(256)'),
('address', 'VARCHAR(256)'),
('name', 'VARCHAR(256)'),
('network_id', 'VARCHAR(256)'),
('model', 'VARCHAR(256)'),
('mac', 'VARCHAR(256)'),
('lan_ip', 'VARCHAR(256)'),
('wan_1_ip', 'VARCHAR(256)'),
('wan_2_ip', 'VARCHAR(256)'),
('tags', 'VARCHAR(256)'),
('lng', 'FLOAT'),
('lat', 'FLOAT'),
]


def get_data(url: str, token: str, params: dict = {}) -> dict:
headers: dict = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Cisco-Meraki-API-Key": f"{token}",
}
try:
log.debug(f"Preparing GET: url={url} with params={params}")
req = requests.get(url, params=params, headers=headers)
req.raise_for_status()
except HTTPError as http_err:
log.error(f"Error GET: url={url}")
log.error(f"HTTP error occurred: {http_err}")
raise
log.debug(req.status_code)
return req.json()


def connect(connection_name, options):
landing_table_client = f'data.meraki_devices_{connection_name}_client_connection'
landing_table_device = f'data.meraki_devices_{connection_name}_device_connection'

comment = yaml_dump(module='meraki_devices', **options)

db.create_table(name=landing_table_client,
cols=LANDING_TABLE_COLUMNS_CLIENT,
comment=comment)
db.execute(f'GRANT INSERT, SELECT ON {landing_table_client} TO ROLE {SA_ROLE}')

db.create_table(name=landing_table_device,
cols=LANDING_TABLE_COLUMNS_DEVICE,
comment=comment)
db.execute(f'GRANT INSERT, SELECT ON {landing_table_device} TO ROLE {SA_ROLE}')
return {
'newStage': 'finalized',
'newMessage': "Meraki ingestion tables created!",
}


def ingest(table_name, options):
ingest_type = 'client' if table_name.endswith('_CLIENT_CONNECTION') else 'device'
landing_table = f'data.{table_name}'

timestamp = datetime.utcnow()
api_token = options['api_token']
whitelist = options['network_id_whitelist']

for network in whitelist:
try:
devices = get_data(f"https://api.meraki.com/api/v0/networks/{network}/devices", api_token)
except requests.exceptions.HTTPError as e:
log.error(f"{network} not accessible, ")
log.error(e)
continue

if ingest_type == 'device':
db.insert(
landing_table,
values=[(
timestamp,
device,
device.get('serial'),
device.get('address'),
device.get('name'),
device.get('networkId'),
device.get('model'),
device.get('mac'),
device.get('lanIp'),
device.get('wan1Ip'),
device.get('wan2Ip'),
device.get('tags'),
device.get('lng'),
device.get('lat'),
) for device in devices],
select=db.derive_insert_select(LANDING_TABLE_COLUMNS_DEVICE),
columns=db.derive_insert_columns(LANDING_TABLE_COLUMNS_DEVICE)
)
log.info(f'Inserted {len(devices)} rows ({landing_table}).')
yield len(devices)

else:
for device in devices:
serial_number = device['serial']

try:
clients = get_data(f"https://api.meraki.com/api/v0/devices/{serial_number}/clients", api_token)
except requests.exceptions.HTTPError as e:
log.error(f"{network} not accessible, ")
log.error(e)
continue

db.insert(
Copy link
Collaborator

@sfc-gh-afedorov sfc-gh-afedorov Sep 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I wasn't more clear on this. could you please replace this db.insert expression with --

            db.insert(
                landing_table,
                values=[(
                    timestamp,
                    client,
                    client.get('id'),
                    client.get('mac'),
                    client.get('description'),
                    client.get('mdnsName'),
                    client.get('dhcpHostname'),
                    client.get('ip'),
                    client.get('switchport'),
                    client.get('vlan') or None,
                    client.get('usage', {}).get('sent') or None,
                    client.get('usage', {}).get('recv') or None,
                    serial_number,
                ) for client in clients],
                select=db.derive_insert_select(LANDING_TABLE_COLUMNS_CLIENT),
                columns=db.derive_insert_columns(LANDING_TABLE_COLUMNS_CLIENT)
            )

I think it will do the same thing but more succinctly.

to clarify x or y will return y if x if "falsy", i.e. if bool(x) is False, and dict's .get returns None as the default value if the key is not present

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, are you sure vlan needs to be treated differently than ip or switchport? this seems off.

Copy link
Contributor

@kuannie1 kuannie1 Sep 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes client.get('vlan') will return '' instead of None, which cannot be inserted into the database since the VLAN column requires a number. I like your suggested change though! I just can't treat vlan the same way as ip because that '' value is invalid in our case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, interesting. If you're sure that the others never return '' then lgtm. Adding a comment like --

                client.get('vlan') or None,  # vlan sometimes set to ''

might help to future folks, as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen that occur with usage:recv or usage:sent, but I think it'll be best if we used the same approach for those instances too. Thank you for this feedback!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sgtm

landing_table,
values=[(
timestamp,
client,
client.get('id'),
client.get('mac'),
client.get('description'),
client.get('mdnsName'),
client.get('dhcpHostname'),
client.get('ip'),
client.get('switchport'),
client.get('vlan') or None, # vlan sometimes set to ''
client.get('usage', {}).get('sent') or None,
client.get('usage', {}).get('recv') or None,
serial_number,
) for client in clients],
select=db.derive_insert_select(LANDING_TABLE_COLUMNS_CLIENT),
columns=db.derive_insert_columns(LANDING_TABLE_COLUMNS_CLIENT)
)
log.info(f'Inserted {len(clients)} rows ({landing_table}).')
yield len(clients)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.