Skip to content

Stairwell/new #2

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

Merged
merged 8 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
39 changes: 0 additions & 39 deletions .github/pull_request_template.md

This file was deleted.

2 changes: 0 additions & 2 deletions .gitignore

This file was deleted.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ If you need help setting up a custom integration, you can create an [issue](http
- [NinjaOne](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ninjaone/)
- [runZero Task Sync](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/task-sync/)
- [Snipe-IT](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snipe-it/)
- [Stairwell](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/)
- [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/)
## Export from runZero
- [runZero Vunerability Workflow](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/)
Expand Down
10 changes: 8 additions & 2 deletions docs/integrations.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lastUpdated": "2025-05-23T20:31:27.935744Z",
"totalIntegrations": 18,
"lastUpdated": "2025-06-02T14:33:02.512708Z",
"totalIntegrations": 19,
"integrationDetails": [
{
"name": "Carbon Black",
Expand Down Expand Up @@ -32,6 +32,12 @@
"readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/lima-charlie/README.md",
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/lima-charlie/custom-integration-lima-charlie.star"
},
{
"name": "Stairwell",
"type": "inbound",
"readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/README.md",
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/custom-integration-stairwell.star"
},
{
"name": "Digital Ocean",
"type": "inbound",
Expand Down
49 changes: 49 additions & 0 deletions stairwell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Custom Integration: Stairwell

## runZero requirements

- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero.

## Stairwell requirements

- API client ID and secret with appropriate permissions.
- Stairwell API URL (e.g. `https://app.stairwell.com`).

## Steps

### Stairwell configuration

1. Generate an API client ID and secret for Stairwell.
- Refer to the [Stairwell API Documentation](https://docs.stairwell.com/reference/) for instructions.
2. Note down the API URL: `https://app.stairwell.com`.

### runZero configuration

1. (OPTIONAL) - Make any necessary changes to the script to align with your environment.
- Modify API calls as needed to filter inventory data.
- Modify datapoints uploaded to runZero as needed.
2. [Create the Credential for the Custom Integration](https://console.runzero.com/credentials).
- Select the type `Custom Integration Script Secrets`.
- For the `access_key`, input your Stairwell client ID.
- For the `access_secret`, input your Stairwell client secret.
3. [Create the Custom Integration](https://console.runzero.com/custom-integrations/new).
- Add a Name and Icon for the integration (e.g., "Stairwell").
- Upload an image file for the Stairwell icon.
- Download [Stairwell logos and icons](https://www.Stairwell.com/wp-content/uploads/2024/10/Stairwell-Logos-and-Favicons.zip)
- Resize selected icon to be 256px by 256px
- Upload resized icon file
- Toggle `Enable custom integration script` to input the finalized script.
- Click `Validate` to ensure it has valid syntax.
- Click `Save` to create the Custom Integration.
4. [Create the Custom Integration task](https://console.runzero.com/ingest/custom/).
- Select the Credential and Custom Integration created in steps 2 and 3.
- Update the task schedule to recur at the desired timeframes.
- Select the Explorer you'd like the Custom Integration to run from.
- Click `Save` to kick off the first task.

### What's next?

- You will see the task kick off on the [tasks](https://console.runzero.com/tasks) page like any other integration.
- The task will update the existing assets with the data pulled from the Custom Integration source.
- The task will create new assets for when there are no existing assets that meet merge criteria (hostname, MAC, etc).
- You can search for assets enriched by this custom integration with the runZero search `custom_integration:Stairwell`.
1 change: 1 addition & 0 deletions stairwell/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "name": "Stairwell", "type": "inbound" }
141 changes: 141 additions & 0 deletions stairwell/custom-integration-stairwell.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
load('runzero.types', 'ImportAsset', 'NetworkInterface')
load('json', json_encode='encode', json_decode='decode')
load('net', 'ip_address')
load('http', http_post='post', http_get='get', 'url_encode')
load('uuid', 'new_uuid')

STAIRWELL_API_URL = 'https://app.stairwell.com'

def get_assets(env, token):
hasNextPage = True
page_size = 5
assets_all = []

url = STAIRWELL_API_URL + "/v1/environments/" + env + "/assets"
headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token}
params = {'limit': page_size}

while hasNextPage:
response = http_get(url, headers=headers, params=params)
if response.status_code != 200:
print('failed to retrieve assets', response.status_code)
return None

assets = json_decode(response.body)

for a in assets.get('assets', ''):
assets_all.append(a)

next_token = assets.get('nextPageToken', '')
if next_token:
params = {'next_page_token': next_token, 'limit': page_size}
else:
hasNextPage = False

return assets_all

def build_assets(assets_json):
imported_assets = []
for item in assets_json:

# parse ip address
ips = []
ip = item.get('ipAddress', '')

# check for no ip address
if not ip:
ip = '127.0.0.1'

# strip interface from ipv6 address
if '%' in ip:
ip = ip.split('%')[0]

ips.append(ip)

# parse mac address
macs = []
mac = item.get('macAddress', '')

if not mac or mac == '-':
continue
else:
macs.append(mac)

# create network interfaces
networks = []
if macs:
for m in macs:
network = build_network_interface(ips=ips, mac=m)
networks.append(network)
else:
network = build_network_interface(ips=ips, mac=None)
networks.append(network)

# parse operating system
os_raw = item.get('os', '')
os_version_raw = item.get('osVersion', '')

if 'macOS' in os_raw:
os = 'macOS ' + os_version_raw
elif 'Ubuntu' in os_raw:
os = 'Ubuntu ' + os_version_raw
elif 'Linux' in os_raw:
os = 'Linux'
else:
os = os_raw

# still need to sort out tag parsing and add logic to convert lastCheckinTime to epoch format

imported_assets.append(
ImportAsset(
id=str(item.get('name', '').split('/')[1]),
hostnames=[item.get('label', '')],
networkInterfaces=networks,
os=os,
osVersion = item.get('osVersion', ''),
customAttributes={
'createTime':item.get('createTime', ''),
'lastCheckinTime':item.get('lastCheckinTime', ''),
'environment':item.get('environment', ''),
'forwarderVersion':item.get('forwarderVersion', ''),
'uploadToken':item.get('uploadToken', ''),
'backscanState':item.get('backscanState', ''),
'os.raw':os_raw,
'state':item.get('state', '')
}
)
)
return imported_assets

# build runZero network interfaces; shouldn't need to touch this
def build_network_interface(ips, mac):
ip4s = []
ip6s = []
for ip in ips[:99]:
ip_addr = ip_address(ip)
if ip_addr.version == 4:
ip4s.append(ip_addr)
elif ip_addr.version == 6:
ip6s.append(ip_addr)
else:
continue
if not mac:
return NetworkInterface(ipv4Addresses=ip4s, ipv6Addresses=ip6s)

return NetworkInterface(macAddress=mac, ipv4Addresses=ip4s, ipv6Addresses=ip6s)

def main(**kwargs):
# kwargs!!
env = kwargs['access_key']
token = kwargs['access_secret']

# get assets
assets = get_assets(env, token)
if not assets:
print('failed to retrieve assets')
return None

# build asset import
imported_assets = build_assets(assets)

return imported_assets