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 44 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 @@ -12,6 +12,7 @@
from . import tenable_settings
from . import crowdstrike_devices
from . import cisco_umbrella
from . import meraki

__all__ = [
'aws_inventory',
Expand All @@ -24,6 +25,7 @@
'azure_vm',
'github_webhooks_s3',
'gsuite_logs',
'meraki',
'okta',
'tenable_settings',
'crowdstrike_devices',
Expand All @@ -41,6 +43,7 @@
'azure_vm': azure_vm,
'github_webhooks_s3': github_webhooks_s3,
'gsuite_logs': gsuite_logs,
'meraki': meraki,
'okta': okta,
'tenable_settings': tenable_settings,
'crowdstrike_devices': crowdstrike_devices,
Expand Down
181 changes: 181 additions & 0 deletions src/connectors/meraki.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Meraki
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's make this name a little more descriptive -- there are a lot of things we could potentially collect from Meraki, and the titles should be distinct.

Collect Meraki Device information using an API Token
"""

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

from datetime import datetime

import snowflake
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': True,
'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 http_err
Copy link
Collaborator

Choose a reason for hiding this comment

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

re-raise with just raise, that way the stack trace is maintained rather than being reset to this point

log.debug(req.status_code)
return req.json()


def connect(connection_name, options):
landing_table_client = f'data.meraki_{connection_name}_connection_client'
Copy link
Contributor

Choose a reason for hiding this comment

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

The table names have to end in _CONNECTION or they're not considered a connection table (i.e. the connection runner won't pick them up and call the ingest method on the module for the table). So you'd want these to be f'data.meraki_{connection_name}_client_connection' or '_device_connection'.

landing_table_device = f'data.meraki_{connection_name}_connection_device'
options['network_id_whitelist'] = options.get('network_id_whitelist', '').split(',')

comment = yaml_dump(module='meraki', **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 parse_number(value):
Copy link
Contributor

Choose a reason for hiding this comment

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

This function seems misnamed; I can pass any non-numerical value and get that value back. It seems like all this does is assert that a value is not None, but we have the value and we can check that ourselves anyway; we don't need to wrap that in a function. What problem are we trying to solve with this function, or what question are we trying to answer about the value we pass in?

if value:
return value
return None


def ingest(table_name, options):
Copy link
Contributor

Choose a reason for hiding this comment

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

So your connect method creates two tables, both of which I think you want to be connection tables. But the ingest method populates both of them every time it gets called, which means that you're going to be duplicating data in your tables; you'll populate both tables when the connection runner finds the client_connection table, and then you'll populate both tables again a second later when the connection runner finds the device_connection table.

For each call to the ingest method, it should populate one table: the table which ingest is called for.

landing_table_client = f'data.{table_name_client}'
landing_table_device = f'data.{landing_table_device}'
timestamp = datetime.utcnow()
api_key = options['api_key']
whitelist = options['network_id_whitelist']

for network in whitelist:

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

for device in devices:
serial_number = device['serial']
clients = get_data(f"https://api.meraki.com/api/v0/devices/{serial_number}/clients", api_key)

db.insert(
landing_table_client,
values=[(
timestamp,
client,
client.get('id','None'),
client.get('mac','None'),
client.get('description','None'),
client.get('mdnsName','None'),
client.get('dhcpHostname','None'),
client.get('ip','None'),
parse_number(client.get('vlan','None')),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't this just be client.get('vlan', None) instead of wrapping it in parse_number?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API is returning empty strings which then error out when trying to parse as a number

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, okay. I'll think about this a bit; I suspect there's a better way to handle this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure what the client value is, but agreed this looks buggy. right now it does:

client.get('vlan', 'None') or None

are you sure this is what you want?

client.get('switchport','None'),
parse_number(client.get('usage','None').get('sent','None')),
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can't .get on a string (i.e. 'None'), so this will raise an exception if 'usage' not in client

parse_number(client.get('usage','None').get('recv','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 (clients).')
yield len(clients)

db.insert(
landing_table_device,
values=[(
timestamp,
device,
device.get('serial','None'),
device.get('address','None'),
device.get('name','None'),
device.get('networkId','None'),
device.get('model','None'),
device.get('mac','None'),
device.get('lanIp','None'),
device.get('wan1Ip','None'),
device.get('wan2Ip','None'),
device.get('tags','None'),
device.get('lng','None'),
device.get('lat','None'),
) 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 (devices).')
yield len(devices)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.