├── stethoscope ├── api │ ├── __init__.py │ ├── endpoints │ │ ├── __init__.py │ │ ├── userinfo.py │ │ ├── feedback.py │ │ ├── accounts.py │ │ ├── events.py │ │ └── notifications.py │ ├── resource.py │ ├── runserver.py │ ├── exceptions.py │ └── utils.py ├── ui │ ├── config.json │ ├── .npmrc │ ├── public │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── accounts │ │ │ │ └── merged │ │ │ │ │ ├── nothing@example.com │ │ │ │ │ ├── user@example.com │ │ │ │ │ ├── green@example.com │ │ │ │ │ ├── one-red@example.com │ │ │ │ │ └── one-unknown@example.com │ │ │ │ ├── devices │ │ │ │ └── merged │ │ │ │ │ └── nothing@example.com │ │ │ │ └── notifications │ │ │ │ └── merged │ │ │ │ ├── nothing@example.com │ │ │ │ ├── green@example.com │ │ │ │ ├── one-red@example.com │ │ │ │ ├── user@example.com │ │ │ │ └── one-unknown@example.com │ │ ├── static │ │ │ ├── favicon.png │ │ │ └── images │ │ │ │ ├── giraffe.png │ │ │ │ └── giraffe-small.png │ │ └── index.html │ ├── src │ │ ├── App.test.js │ │ ├── Device.test.js │ │ ├── Overview.test.js │ │ ├── utils │ │ │ ├── sortby.js │ │ │ └── device-filter.js │ │ ├── App.css │ │ ├── Accounts.test.js │ │ ├── nav.css │ │ ├── Action.test.js │ │ ├── Loader.js │ │ ├── Spinner.js │ │ ├── Notification.css │ │ ├── notifications │ │ │ ├── NotificationHandlers.js │ │ │ └── Simple.js │ │ ├── Devices.css │ │ ├── Notification.test.js │ │ ├── ShowMore.js │ │ ├── Config.js │ │ ├── CriticalDeviceSummary.js │ │ ├── Accounts.css │ │ ├── Faq.js │ │ ├── Loader.css │ │ ├── Tracker.js │ │ ├── index.js │ │ ├── index-mobile.css │ │ ├── Devices.js │ │ ├── ActionIcon.js │ │ ├── Devices.test.js │ │ ├── Overview.js │ │ ├── Spinner.css │ │ ├── Notification.js │ │ ├── Accessible.test.js │ │ ├── Accounts.js │ │ ├── Device.css │ │ ├── Accessible.js │ │ ├── Action.js │ │ └── DataStore.js │ ├── config.defaults.json │ └── package.json ├── batch │ ├── __init__.py │ └── plugins │ │ ├── __init__.py │ │ ├── summary │ │ ├── __init__.py │ │ └── restful.py │ │ └── incremental │ │ ├── __init__.py │ │ └── es.py ├── login │ ├── __init__.py │ ├── plugins │ │ ├── __init__.py │ │ ├── base.py │ │ └── oidc.py │ ├── manage.py │ ├── defaults.py │ └── factory.py ├── plugins │ ├── __init__.py │ ├── mixins │ │ ├── __init__.py │ │ ├── es.py │ │ ├── deferred_http.py │ │ └── http.py │ ├── feedback │ │ ├── __init__.py │ │ └── restful.py │ ├── logging │ │ ├── __init__.py │ │ ├── atlas.py │ │ └── eslogger.py │ ├── sources │ │ ├── __init__.py │ │ ├── duo │ │ │ ├── __init__.py │ │ │ ├── deferred.py │ │ │ ├── base.py │ │ │ └── utils.py │ │ ├── bitfit │ │ │ ├── __init__.py │ │ │ ├── utils.py │ │ │ ├── base.py │ │ │ └── deferred.py │ │ ├── google │ │ │ ├── __init__.py │ │ │ ├── deferred.py │ │ │ └── utils.py │ │ ├── jamf │ │ │ ├── __init__.py │ │ │ └── utils.py │ │ ├── landesk │ │ │ ├── __init__.py │ │ │ └── deferred.py │ │ └── esnotifications.py │ ├── transform │ │ ├── __init__.py │ │ ├── vpnlabeler.py │ │ ├── refilter.py │ │ └── macmanufacturer.py │ └── utils.py ├── __init__.py ├── exceptions.py ├── token.py ├── configurator.py ├── utils.py ├── hostchecker.py ├── connectivity.py └── auth.py ├── MANIFEST.in ├── OSSMETADATA ├── AUTHORS ├── docs ├── images │ └── screenshot.png ├── jamf_extension_attributes │ ├── remote_login.sh │ ├── firewall_status.sh │ ├── wireless_mac_address.sh │ ├── screen_saver_lock.sh │ ├── 3-install_app_updates.sh │ ├── 1-auto_check_for_updates.sh │ ├── 6-install_system_data_files.sh │ ├── 4-install_os_x_updates.sh │ ├── 2-get_new_updates_in_background.sh │ └── 5-install_security_updates.sh ├── nginx.rst ├── Makefile ├── index.rst ├── frontend.rst └── backend.rst ├── Dockerfile-node ├── Dockerfile-nginx ├── .dockerignore ├── .editorconfig ├── .travis └── install.sh ├── .gitignore ├── .travis.yml ├── Dockerfile ├── docker-compose.yml ├── config ├── api │ └── production.py └── login │ └── production.py ├── setup.cfg ├── instance └── config.py ├── tests ├── fixtures │ ├── google │ │ └── devices │ │ │ ├── ios_google-sync.json │ │ │ ├── android.json │ │ │ ├── ios_ios-sync.json │ │ │ ├── chromeos.json │ │ │ └── chromeos_pixelbook.json │ └── bitfit │ │ └── devices_response.json ├── test_deferred_http.py ├── test_refilter.py ├── test_bitfit_networking.py ├── test_jamf_networking.py ├── test_hostchecker.py └── test_jamf.py ├── docker-compose.base.yml ├── requirements.txt ├── tox.ini ├── nginx-docker.conf ├── nginx.conf ├── CONTRIBUTING.md ├── README.md └── Makefile /stethoscope/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/ui/config.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /stethoscope/batch/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/login/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=active 2 | -------------------------------------------------------------------------------- /stethoscope/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/batch/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/login/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/feedback/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/logging/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/duo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/transform/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/batch/plugins/summary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/bitfit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/google/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/jamf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/landesk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/batch/plugins/incremental/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stethoscope/ui/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/accounts/merged/nothing@example.com: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/devices/merged/nothing@example.com: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/notifications/merged/nothing@example.com: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | - Andrew M. White 2 | - Jesse Kriss 3 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/stethoscope/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/remote_login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "`/usr/sbin/systemsetup -getremotelogin | awk '{print $3}'`" -------------------------------------------------------------------------------- /Dockerfile-node: -------------------------------------------------------------------------------- 1 | FROM node:7-alpine 2 | 3 | ADD stethoscope/ui /code/stethoscope/ui 4 | 5 | WORKDIR /code/stethoscope/ui 6 | 7 | RUN npm install -------------------------------------------------------------------------------- /stethoscope/ui/public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/stethoscope/HEAD/stethoscope/ui/public/static/favicon.png -------------------------------------------------------------------------------- /Dockerfile-nginx: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY nginx-docker.conf /etc/nginx/nginx.conf 4 | RUN mkdir -p /logs 5 | 6 | ADD stethoscope /code/stethoscope/ 7 | -------------------------------------------------------------------------------- /stethoscope/ui/public/static/images/giraffe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/stethoscope/HEAD/stethoscope/ui/public/static/images/giraffe.png -------------------------------------------------------------------------------- /stethoscope/ui/public/static/images/giraffe-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/stethoscope/HEAD/stethoscope/ui/public/static/images/giraffe-small.png -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/firewall_status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fwResult=`/usr/bin/defaults read /Library/Preferences/com.apple.alf globalstate` 4 | 5 | echo "$fwResult" -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/notifications/merged/green@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "simple-01", 4 | "type": "simple", 5 | "message": "This is an example alert for green@example.com." 6 | } 7 | ] -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/notifications/merged/one-red@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "simple-01", 4 | "type": "simple", 5 | "message": "This is an example alert for one-red@example.com." 6 | } 7 | ] -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/notifications/merged/user@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "simple-01", 4 | "type": "simple", 5 | "message": "This is an example alert for user@example.com." 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | *.pyc 4 | *.pyo 5 | *.tmp 6 | .eggs/ 7 | .git/ 8 | .tox/ 9 | .ropeproject/ 10 | .cache/ 11 | Stethoscope.egg-info/ 12 | build/ 13 | htmlcov/ 14 | scratch/ 15 | tmp/ 16 | twistd.pid 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | max_line_length = 100 11 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/notifications/merged/one-unknown@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "simple-01", 4 | "type": "simple", 5 | "message": "This is an example alert for one-unknown@example.com." 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /stethoscope/api/resource.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import stethoscope.api.factory 6 | 7 | 8 | app = stethoscope.api.factory.create_app() 9 | resource = app.resource 10 | -------------------------------------------------------------------------------- /stethoscope/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from pkg_resources import DistributionNotFound, get_distribution 4 | 5 | 6 | try: 7 | __version__ = get_distribution(__name__).version 8 | except DistributionNotFound: 9 | # package is not installed 10 | pass 11 | -------------------------------------------------------------------------------- /stethoscope/api/runserver.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | from .factory import create_app 6 | 7 | 8 | if __name__ == "__main__": 9 | app = create_app() 10 | app.run("127.0.0.1", 5001) 11 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/wireless_mac_address.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | wifiPort=`networksetup -listallhardwareports | awk '/Hardware Port: Wi-Fi/,/Ethernet/' | awk 'NR==2' | cut -d " " -f 2` 4 | macAddy=`networksetup -getmacaddress $wifiPort | awk {'print $3'}` 5 | 6 | echo "$macAddy" -------------------------------------------------------------------------------- /stethoscope/ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | ReactDOM.render(, div) 10 | }) 11 | -------------------------------------------------------------------------------- /.travis/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -u 5 | 6 | case "${TEST_SUITE}" in 7 | js) 8 | make install-node-requirements; 9 | ;; 10 | py) 11 | make install-python-requirements; 12 | ;; 13 | *) 14 | echo "Unknown test suite: $TEST_SUITE" 15 | exit 1 16 | esac; 17 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Device.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import Device from './Device' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | ReactDOM.render(, div) 10 | }) 11 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/accounts/merged/user@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": "google", 4 | "last_updated": "2016-12-31T00:25:15.616802+00:00", 5 | "user_usage": { 6 | "accounts:is_2sv_enrolled": true 7 | }, 8 | "type": "google", 9 | "name": "user@example.com" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Overview.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import Overview from './Overview' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | ReactDOM.render(, div) 10 | }) 11 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/accounts/merged/green@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": "google", 4 | "last_updated": "2016-12-31T00:25:15.616802+00:00", 5 | "user_usage": { 6 | "accounts:is_2sv_enrolled": true 7 | }, 8 | "type": "google", 9 | "name": "green@example.com" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/accounts/merged/one-red@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": "google", 4 | "last_updated": "2016-12-31T00:25:15.616802+00:00", 5 | "user_usage": { 6 | "accounts:is_2sv_enrolled": false 7 | }, 8 | "type": "google", 9 | "name": "one-red@example.com" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /stethoscope/ui/src/utils/sortby.js: -------------------------------------------------------------------------------- 1 | export default function sortBy (collection, valueFunction) { 2 | return collection.slice(0).sort((a, b) => { 3 | const valA = valueFunction(a) 4 | const valB = valueFunction(b) 5 | if (valA < valB) return -1 6 | if (valA > valB) return 1 7 | return 0 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /stethoscope/ui/public/api/v1/accounts/merged/one-unknown@example.com: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": "google", 4 | "last_updated": "2016-12-31T00:25:15.616802+00:00", 5 | "user_usage": { 6 | "accounts:is_2sv_enrolled": false 7 | }, 8 | "type": "google", 9 | "name": "one-unknown@example.com" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /stethoscope/ui/src/App.css: -------------------------------------------------------------------------------- 1 | .callout-danger { 2 | border-left: 4px solid #b9090b; 3 | margin: 0 10px 20px 10px; 4 | font-size: 16px; 5 | } 6 | 7 | .more-info { 8 | padding-left: 15px; 9 | border-left: 2px solid #eee; 10 | font-size: 14px; 11 | margin: 10px 0; 12 | } 13 | 14 | .more-info code { 15 | font-size: 0.9em; 16 | } -------------------------------------------------------------------------------- /stethoscope/ui/src/Accounts.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import Accounts from './Accounts' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | const store = { accounts: [] } 10 | ReactDOM.render(, div) 11 | }) 12 | -------------------------------------------------------------------------------- /stethoscope/ui/src/nav.css: -------------------------------------------------------------------------------- 1 | nav { 2 | padding: 0 20px; 3 | height: 50px; 4 | } 5 | 6 | a.active { 7 | border-top: 4px solid #aaa; 8 | position: relative; 9 | } 10 | 11 | nav ul li { 12 | padding: 0; 13 | } 14 | 15 | nav ul li a { 16 | margin: 0 5px; 17 | padding: 10px 15px; 18 | box-sizing: content-box; 19 | font-weight: 300; 20 | font-size: 17px; 21 | } -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/screen_saver_lock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lastUser=`defaults read /Library/Preferences/com.apple.loginwindow lastUserName` 3 | passwordStatus=`defaults read /Users/$lastUser/Library/Preferences/com.apple.screensaver askForPassword` 4 | 5 | if [ "$passwordStatus" == "0" ]; then 6 | echo "Disabled" 7 | else 8 | echo "Enabled" 9 | fi -------------------------------------------------------------------------------- /stethoscope/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Loading... 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Action.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import Action from './Action' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | const action = { 10 | title: 'Test action' 11 | } 12 | ReactDOM.render(, div) 13 | }) 14 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/3-install_app_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | autoUpdate=`defaults read /Library/Preferences/com.apple.commerce.plist AutoUpdate` 4 | if [ ${autoUpdate} = 1 ]; then 5 | echo "True" 6 | elif [ ${autoUpdate} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.log 3 | *.pyc 4 | *.tmp 5 | .DS_Store 6 | .cache/ 7 | .coverage 8 | .eggs/ 9 | .python-version 10 | .tox/ 11 | venv 12 | Stethoscope.egg-info/ 13 | /htmlcov/ 14 | node_modules/ 15 | scratch/ 16 | tmp/ 17 | twistd.pid 18 | .env 19 | /stethoscope/static/* 20 | /stethoscope/login/templates/layout.html 21 | /docs/_build 22 | /test-reports/ 23 | /build/ 24 | /dist/ 25 | /venv/ 26 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/1-auto_check_for_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | autoCheck=`defaults read /Library/Preferences/com.apple.SoftwareUpdate.plist AutomaticCheckEnabled` 4 | if [ ${autoCheck} = 1 ]; then 5 | echo "True" 6 | elif [ ${autoCheck} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/6-install_system_data_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | configData=`defaults read /Library/Preferences/com.apple.SoftwareUpdate.plist ConfigDataInstall` 4 | if [ ${configData} = 1 ]; then 5 | echo "True" 6 | elif [ ${configData} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/4-install_os_x_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | updateRestart=`defaults read /Library/Preferences/com.apple.commerce.plist AutoUpdateRestartRequired` 4 | if [ ${updateRestart} = 1 ]; then 5 | echo "True" 6 | elif [ ${updateRestart} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Loader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './Loader.css' 3 | 4 | class Loader extends Component { 5 | render () { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | ) 13 | } 14 | } 15 | 16 | export default Loader 17 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/2-get_new_updates_in_background.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | autoDownload=`defaults read /Library/Preferences/com.apple.SoftwareUpdate.plist AutomaticDownload` 4 | if [ ${autoDownload} = 1 ]; then 5 | echo "True" 6 | elif [ ${autoDownload} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './Spinner.css' 3 | 4 | class Spinner extends Component { 5 | render () { 6 | return ( 7 |
8 |
Loading...
9 |
{this.props.children}
10 |
11 | ) 12 | } 13 | } 14 | 15 | export default Spinner 16 | -------------------------------------------------------------------------------- /docs/jamf_extension_attributes/5-install_security_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | criticalUpdate=`defaults read /Library/Preferences/com.apple.SoftwareUpdate.plist CriticalUpdateInstall` 4 | if [ ${criticalUpdate} = 1 ]; then 5 | echo "True" 6 | elif [ ${criticalUpdate} = 0 ]; then 7 | echo "False" 8 | else 9 | echo "Unknown Status" 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /stethoscope/exceptions.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import werkzeug.exceptions 6 | 7 | 8 | class ValidationError(werkzeug.exceptions.BadRequest): 9 | 10 | def __init__(self, quantity, value, **kwargs): 11 | msg = "Invalid {!s}: '{!s}'.".format(quantity, value) 12 | super(ValidationError, self).__init__(msg, **kwargs) 13 | -------------------------------------------------------------------------------- /stethoscope/login/manage.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | from flask_script import Manager, Server, Shell 6 | 7 | import stethoscope.login.factory 8 | 9 | 10 | manager = Manager(stethoscope.login.factory.create_app) 11 | 12 | 13 | def main(): 14 | manager.add_command("runserver", Server()) 15 | manager.add_command("shell", Shell()) 16 | manager.run() 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /stethoscope/login/defaults.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | 7 | import stethoscope.utils 8 | 9 | 10 | LOGFILE = os.environ.get('STETHOSCOPE_LOGIN_LOGFILE', os.environ.get('LOGFILE', 'login.log')) 11 | 12 | LOGBOOK = stethoscope.utils.setup_logbook(LOGFILE, logfile_kwargs={'delay': True}) 13 | 14 | DEBUG = True 15 | TESTING = True 16 | 17 | JWT_ALGORITHM = 'HS256' 18 | JWT_EXPIRATION_DELTA = 60 * 60 * 24 19 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Notification.css: -------------------------------------------------------------------------------- 1 | .notification .additional-comments { 2 | margin: 5px 0 10px; 3 | width: 100%; 4 | height: 5em; 5 | } 6 | .notification .help-text { 7 | margin: 5px 0 !important; 8 | padding: 5px 8px; 9 | background: #fbfbe5; 10 | border: 1px solid #e8e8ae; 11 | line-height: 1.5em; 12 | color: #505029; 13 | } 14 | .notification .answer-selected .btn { 15 | opacity: 0.3; 16 | } 17 | .notification .answer-selected .btn.selected { 18 | opacity: 1; 19 | } 20 | .notification a.toggle-details { 21 | color: #666; 22 | font-size: 0.8em; 23 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | env: TEST_SUITE=py 8 | matrix: 9 | include: 10 | env: TEST_SUITE=js 11 | addons: 12 | apt: 13 | packages: 14 | - freetds-dev 15 | cache: 16 | pip: true 17 | directories: 18 | - ./stethoscope/ui/node_modules 19 | before_install: 20 | - nvm install 6.9.5 21 | install: 22 | - pip install tox-travis 23 | - if [[ "$TEST_SUITE" == "js" ]]; then make install-node-requirements; fi 24 | script: 25 | - if [[ "$TEST_SUITE" == "js" ]]; then make test-js; else tox; fi 26 | -------------------------------------------------------------------------------- /stethoscope/ui/config.defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName" : "Stethoscope", 3 | "orgName" : "SomeCorp", 4 | "pages" : ["overview", "devices", "accounts", "faq"], 5 | "hiddenPractices" : [], 6 | "contactEmail" : "contact@example.com", 7 | "sourceLabels" : { 8 | "google": "Google Apps", 9 | "wirelesslogs": "wireless logs", 10 | "bitfit": "the hardware inventory system" 11 | }, 12 | "faq" : { 13 | "content" : [ 14 | { 15 | "question": "What is this?", 16 | "answer": "This is Stethoscope, a tool for viewing the security state of your devices." 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /docs/nginx.rst: -------------------------------------------------------------------------------- 1 | Nginx 2 | ===== 3 | 4 | Configuration 5 | ------------- 6 | 7 | For `Nginx`_, the supplied :file:`nginx.conf` sets up the appropriate configuration for running 8 | locally out of the repository directory. Essentially, requests for static files are handled by 9 | `Nginx`_, requests for non-API endpoints are proxied to port 5002 (to be handled by the login 10 | server), and requests for API endpoints are proxied to port 5001 (to be handled by the API server). 11 | 12 | Running 13 | ------- 14 | 15 | .. code:: sh 16 | 17 | nginx -c nginx.conf -p $(pwd) 18 | 19 | 20 | .. _Nginx: https://www.nginx.com/ 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | # see: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#build-cache#run 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends freetds-dev \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | WORKDIR /code 9 | ADD Makefile ./ 10 | 11 | # add requirements.txt before rest of repo so Docker will cache pip install 12 | ADD requirements.txt ./ 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | ADD README.md setup.cfg setup.py ./ 16 | ADD stethoscope ./stethoscope/ 17 | ADD instance ./instance/ 18 | ADD config ./config/ 19 | 20 | RUN make install-editable-package 21 | -------------------------------------------------------------------------------- /stethoscope/ui/src/notifications/NotificationHandlers.js: -------------------------------------------------------------------------------- 1 | if (typeof global.stethoscopeNotificationHandlers === 'undefined') { 2 | global.stethoscopeNotificationHandlers = [] 3 | } 4 | 5 | const components = global.stethoscopeNotificationHandlers 6 | 7 | function register (component) { 8 | components.push(component) 9 | } 10 | 11 | function all () { 12 | return components 13 | } 14 | 15 | function getComponentForData (data) { 16 | const components = all() 17 | for (let i = 0; i < components.length; i++) { 18 | const c = components[i] 19 | if (c.handles(data)) return c 20 | } 21 | } 22 | 23 | module.exports = { register, all, getComponentForData } 24 | -------------------------------------------------------------------------------- /stethoscope/api/endpoints/userinfo.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import logbook 6 | 7 | import stethoscope.plugins.utils 8 | from stethoscope.api.endpoints.utils import add_get_route 9 | 10 | 11 | logger = logbook.Logger(__name__) 12 | 13 | 14 | def register_userinfo_api_endpoints(app, config, auth, log_hooks=[]): 15 | userinfo_plugins = stethoscope.plugins.utils.instantiate_plugins(config, 16 | namespace='stethoscope.plugins.sources.userinfo') 17 | 18 | if config['DEBUG']: 19 | userinfo_plugins.map(add_get_route, app, auth, 'userinfo', 'email', log_hooks=log_hooks) 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | login: 4 | extends: 5 | file: docker-compose.base.yml 6 | service: login 7 | api: 8 | extends: 9 | file: docker-compose.base.yml 10 | service: api 11 | links: 12 | - login 13 | nginx: 14 | extends: 15 | file: docker-compose.base.yml 16 | service: nginx 17 | links: 18 | - login 19 | - api 20 | node-builder: 21 | build: 22 | context: . 23 | dockerfile: Dockerfile-node 24 | volumes: 25 | - ./stethoscope/ui/src:/code/stethoscope/ui/src 26 | - ./stethoscope/static:/code/stethoscope/static 27 | - ./stethoscope/login:/code/stethoscope/login 28 | command: ["npm", "run", "buildonly"] -------------------------------------------------------------------------------- /stethoscope/ui/src/notifications/Simple.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class SimpleNotification extends Component { 4 | 5 | constructor (opts) { 6 | super(opts) 7 | this.state = { 8 | showComment: false 9 | } 10 | } 11 | 12 | dismiss () { 13 | if (this.props.onDismiss) this.props.onDismiss() 14 | } 15 | 16 | render () { 17 | return ( 18 |
19 |

{this.props.data.message}

20 | 21 |
22 | ) 23 | } 24 | } 25 | 26 | SimpleNotification.handles = function (data) { 27 | return data.type === 'simple' 28 | } 29 | 30 | export default SimpleNotification 31 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/bitfit/utils.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | 6 | def get_photo_url(raw): 7 | image_url = raw['item'].get('image_url', raw['item']['config'].get('image_url')) 8 | photo_id = raw['item'].get('photo_id', raw['item']['config'].get('photo_id')) 9 | if image_url is not None: 10 | return image_url 11 | elif photo_id is not None: 12 | return 'https://app.bitfit.com/api/attachments/{:d}/download?scale=tiny'.format(photo_id) 13 | else: 14 | return None 15 | 16 | 17 | def parse_field_list(fields): 18 | retval = dict() 19 | for field in fields: 20 | retval[field['base_name']] = field 21 | return retval 22 | -------------------------------------------------------------------------------- /stethoscope/api/endpoints/feedback.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import logbook 6 | 7 | import stethoscope.plugins.utils 8 | from stethoscope.api.endpoints.utils import add_post_route 9 | 10 | 11 | logger = logbook.Logger(__name__) 12 | 13 | 14 | def register_feedback_api_endpoints(app, config, auth, csrf, log_hooks=[]): 15 | feedback_plugins = stethoscope.plugins.utils.instantiate_plugins(config, 16 | namespace='stethoscope.plugins.feedback') 17 | 18 | if config.get('ENABLE_FEEDBACK_ENDPOINTS', config['DEBUG']) \ 19 | and len(feedback_plugins.names()) > 0: 20 | feedback_plugins.map(add_post_route, app, config, auth, csrf, 'feedback') 21 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Devices.css: -------------------------------------------------------------------------------- 1 | a.toggleLink { 2 | font-size: 0.8em; 3 | color: #ddd; 4 | } 5 | 6 | a.toggleLink.closed { 7 | -ms-transform: rotate(-90deg); /* IE 9 */ 8 | -webkit-transform: rotate(-90deg); /* Chrome, Safari, Opera */ 9 | transform: rotate(90deg); 10 | backface-visibility: hidden; 11 | } 12 | 13 | @media only screen and (min-width: 600px) { 14 | #device-list { 15 | column-count: 2; 16 | -moz-column-count: 2; 17 | } 18 | } 19 | @media only screen and (min-width: 1000px) { 20 | #device-list { 21 | column-count: 3; 22 | -moz-column-count: 3; 23 | } 24 | } 25 | @media only screen and (min-width: 2000px) { 26 | #device-list { 27 | column-count: 4; 28 | -moz-column-count: 4; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/api/production.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | 7 | import logbook 8 | 9 | 10 | LOGLEVEL = os.environ.get('STETHOSCOPE_API_LOGLEVEL', os.environ.get('LOGLEVEL', 'INFO')).upper() 11 | LOGFILE = os.environ.get('STETHOSCOPE_API_LOGFILE', os.environ.get('LOGFILE', 'api.log')) 12 | 13 | LOGBOOK = logbook.NestedSetup([ 14 | logbook.NullHandler(), 15 | logbook.MonitoringFileHandler(LOGFILE, mode='a', level=LOGLEVEL, 16 | format_string=('[{record.time} {record.level_name:<8s} {record.channel:>10s} |' 17 | ' {record.filename:s}:{record.lineno:d}] {record.message:s}')), 18 | ]) 19 | 20 | DEBUG = False 21 | TESTING = False 22 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Notification.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import React, { Component } from 'react' 4 | import ReactDOM from 'react-dom' 5 | import Notification from './Notification' 6 | 7 | class TestNotification extends Component { 8 | render () { 9 | return
{this.props.data.description}
10 | } 11 | } 12 | TestNotification.handles = function (data) { 13 | return data.type && data.type === 'TestNotification' 14 | } 15 | 16 | it('renders without crashing', () => { 17 | const div = document.createElement('div') 18 | const data = { 19 | description: 'Generic alert for example user', 20 | type: 'TestNotification' 21 | } 22 | ReactDOM.render(, div) 23 | }) 24 | -------------------------------------------------------------------------------- /stethoscope/ui/src/utils/device-filter.js: -------------------------------------------------------------------------------- 1 | export function criticalOnly (devices) { 2 | return devices ? devices.filter((d) => d.deviceRating === 'critical') : [] 3 | } 4 | 5 | export function criticalDeviceSummary (devices) { 6 | const criticalDevices = criticalOnly(devices) 7 | if (criticalDevices.length > 0) { 8 | const count = criticalDevices.length 9 | const pluralizedDevices = 'device' + (count !== 1 ? 's' : '') 10 | const pluralizedRequires = 'require' + (count === 1 ? 's' : '') 11 | const devicesList = criticalDevices.map(function (d) { 12 | return d.model || d.manufacturer 13 | }).join(', ') 14 | 15 | return { 16 | count, 17 | pluralizedDevices, 18 | pluralizedRequires, 19 | devicesList 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Stethoscope 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | livehtml: 16 | sphinx-autobuild -b html "$(SPHINXOPTS)" "$(BUILDDIR)/html" 17 | 18 | .PHONY: help livehtml Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /stethoscope/ui/src/ShowMore.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class ShowMore extends Component { 4 | constructor (opts) { 5 | super(opts) 6 | this.state = { 7 | expanded: false 8 | } 9 | } 10 | toggle (evt) { 11 | evt.preventDefault() 12 | this.setState({ expanded: !this.state.expanded }) 13 | } 14 | render () { 15 | const linkText = this.state.expanded ? (this.props.hideText || 'hide') : (this.props.showText || 'show more') 16 | const link = this.toggle(evt)}>{linkText} 17 | let content 18 | if (this.state.expanded) content =
{this.props.children}
19 | return (
20 | {link} 21 | {content} 22 |
) 23 | } 24 | } 25 | 26 | export default ShowMore 27 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Config.js: -------------------------------------------------------------------------------- 1 | let config = require('../config.defaults.json') 2 | config = Object.assign(config, require('../config.json')) 3 | 4 | // also apply environment variable overrides, if present 5 | if (process.env.REACT_APP_CONFIG && process.env.REACT_APP_CONFIG.trim() !== '') { 6 | try { 7 | const moreConfig = JSON.parse(process.env.REACT_APP_CONFIG) 8 | config = Object.assign(config, moreConfig) 9 | } catch (e) { 10 | console.log('Error applying more config from environment variables:', e) 11 | } 12 | } 13 | 14 | // set the cookie if it's provided 15 | if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_TOKEN) { 16 | document.cookie = `token=${process.env.REACT_APP_TOKEN}` 17 | console.log('set token to', process.env.REACT_APP_TOKEN) 18 | } 19 | 20 | export default config 21 | -------------------------------------------------------------------------------- /stethoscope/plugins/sources/duo/deferred.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import logbook 6 | from twisted.internet import threads 7 | 8 | import stethoscope.configurator 9 | import stethoscope.plugins.sources.duo.base 10 | 11 | 12 | logger = logbook.Logger(__name__) 13 | 14 | 15 | class DeferredDuoDataSource( 16 | stethoscope.plugins.sources.duo.base.DuoDataSourceBase, 17 | stethoscope.configurator.Configurator, 18 | ): 19 | 20 | @property 21 | def connection(self): 22 | # TODO: fix so that every request doesn't require a new connection 23 | return self.connect() 24 | 25 | def get_events_by_email(self, email): 26 | return threads.deferToThread(super(DeferredDuoDataSource, self).get_events_by_email, email) 27 | -------------------------------------------------------------------------------- /stethoscope/ui/src/CriticalDeviceSummary.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { criticalDeviceSummary } from './utils/device-filter' 3 | import Accessible from './Accessible' 4 | 5 | export default (props) => { 6 | const devices = props.devices || props.store.devices 7 | if (!devices) return null 8 | const summary = criticalDeviceSummary(devices) 9 | 10 | if (summary) { 11 | return ( 12 | 13 |

14 | You have {summary.count} {summary.pluralizedDevices} that {summary.pluralizedRequires} attention: 15 |   16 | 17 | {summary.devicesList} 18 | 19 |

20 |
21 | ) 22 | } else { 23 | return null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /stethoscope/plugins/transform/vpnlabeler.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import logbook 6 | import netaddr 7 | 8 | import stethoscope.configurator 9 | 10 | 11 | logger = logbook.Logger(__name__) 12 | 13 | 14 | class VPNLabeler(stethoscope.configurator.Configurator): 15 | 16 | config_keys = ( 17 | 'VPN_CIDRS', 18 | ) 19 | 20 | def __init__(self, *args, **kwargs): 21 | super(VPNLabeler, self).__init__(*args, **kwargs) 22 | self._networks = netaddr.IPSet(self.config['VPN_CIDRS']) 23 | 24 | def transform(self, events): 25 | """Augment each event with a tag indicating whether the associated IP is in the VPN range.""" 26 | for event in events: 27 | event['vpn'] = (netaddr.IPAddress(event['ip_address']) in self._networks) 28 | return events 29 | -------------------------------------------------------------------------------- /config/login/production.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | 7 | import logbook 8 | 9 | 10 | LOGLEVEL = os.environ.get('STETHOSCOPE_LOGIN_LOGLEVEL', os.environ.get('LOGLEVEL', 'INFO')).upper() 11 | LOGFILE = os.environ.get('STETHOSCOPE_LOGIN_LOGFILE', os.environ.get('LOGFILE', 'login.log')) 12 | 13 | LOGBOOK = logbook.NestedSetup([ 14 | logbook.NullHandler(), 15 | logbook.MonitoringFileHandler(LOGFILE, mode='a', level=LOGLEVEL, 16 | format_string=('[{record.time} {record.level_name:<8s} {record.channel:>10s} |' 17 | ' {record.filename:s}:{record.lineno:d}] {record.message:s}')), 18 | ]) 19 | 20 | SESSION_COOKIE_HTTPONLY = True 21 | SESSION_COOKIE_SECURE = True 22 | 23 | CSRF_COOKIE_HTTPONLY = False # unnecessary 24 | CSRF_COOKIE_SECURE = True 25 | 26 | DEBUG = False 27 | TESTING = False 28 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Accounts.css: -------------------------------------------------------------------------------- 1 | .account-wrapper { 2 | width: 100%; 3 | box-sizing: border-box; 4 | padding: 10px; 5 | display: inline-block; 6 | } 7 | .panel.account { 8 | box-sizing: border-box; 9 | display: inline-block; 10 | vertical-align: top; 11 | margin-bottom: 15px; 12 | width: 100%; 13 | } 14 | .account-type { 15 | font-weight: bold; 16 | text-transform: capitalize; 17 | } 18 | .account ul { 19 | list-style: none; 20 | padding-left: 0; 21 | margin: 0; 22 | } 23 | @media only screen and (min-width: 600px) { 24 | #account-list { 25 | column-count: 2; 26 | -moz-column-count: 2; 27 | } 28 | } 29 | @media only screen and (min-width: 1000px) { 30 | #account-list { 31 | column-count: 3; 32 | -moz-column-count: 3; 33 | } 34 | } 35 | @media only screen and (min-width: 2000px) { 36 | #account-list { 37 | column-count: 4; 38 | -moz-column-count: 4; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bdist_wheel] 11 | universal = 1 12 | 13 | [flake8] 14 | ignore = E111,E114,E121,E125,E128,W504 15 | exclude = .git,__pycache__,*.pyc,*.pyo,.tox,.eggs,*.egg-info,.ropeproject,scratch 16 | max-line-length = 100 17 | doctests = True 18 | import-order-style = smarkets 19 | application-import-names = stethoscope 20 | 21 | [pep8] 22 | ignore = E111,E114,E121,E125,E128 23 | max_line_length = 100 24 | 25 | [pylama:pep8] 26 | max_line_length = 100 27 | 28 | [tool:pytest] 29 | norecursedirs = .* *.egg *.egg-info instance stethoscope/static config node_modules htmlcov scratch tmp 30 | addopts = --doctest-modules --ignore=stethoscope/api/resource.py --ignore=setup.py --ignore=docs/conf.py 31 | doctest_optionflags = ALLOW_UNICODE ELLIPSIS 32 | -------------------------------------------------------------------------------- /stethoscope/token.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import argparse 6 | 7 | import arrow 8 | 9 | import stethoscope.api.factory 10 | import stethoscope.auth 11 | 12 | 13 | def _main(args): 14 | config = stethoscope.api.factory.get_config() 15 | provider = stethoscope.auth.AuthProvider(config) 16 | print(provider.create_token(**{ 17 | 'sub': args.sub, 18 | 'exp': arrow.utcnow().replace(days=+args.days_valid).datetime, 19 | })) 20 | 21 | 22 | def main(): 23 | parser = argparse.ArgumentParser(description=( 24 | "Generate a bearer token for auth with Stethoscope's API." 25 | )) 26 | parser.add_argument("--sub", dest="sub", type=str, default='*') 27 | parser.add_argument("--days-valid-for", dest="days_valid", type=int, default=1) 28 | _main(parser.parse_args()) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /stethoscope/ui/src/Faq.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Config from './Config.js' 3 | 4 | class Faq extends Component { 5 | render () { 6 | let faqContent = null 7 | if (Config.faq.googleDoc) { 8 | faqContent =