├── .gitignore ├── Dockerfile ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-36.pyc │ ├── __init__.cpython-38.pyc │ ├── __init__.cpython-39.pyc │ ├── api_exceptions.cpython-36.pyc │ ├── api_exceptions.cpython-38.pyc │ ├── blueprints.cpython-36.pyc │ ├── blueprints.cpython-38.pyc │ ├── main.cpython-38.pyc │ ├── main.cpython-39.pyc │ └── not_main.cpython-38.pyc ├── common │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ ├── __init__.cpython-39.pyc │ │ ├── constants.cpython-38.pyc │ │ ├── constants.cpython-39.pyc │ │ ├── github_event.cpython-38.pyc │ │ ├── github_event.cpython-39.pyc │ │ ├── logging.cpython-38.pyc │ │ ├── logging.cpython-39.pyc │ │ ├── secrets_loader.cpython-38.pyc │ │ ├── secrets_loader.cpython-39.pyc │ │ ├── snyk_models.cpython-39.pyc │ │ ├── utils.cpython-38.pyc │ │ ├── utils.cpython-39.pyc │ │ ├── webhook_validator.cpython-38.pyc │ │ └── webhook_validator.cpython-39.pyc │ ├── constants.py │ ├── logging.py │ └── utils.py ├── github │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ ├── __init__.cpython-39.pyc │ │ ├── config.cpython-38.pyc │ │ ├── config.cpython-39.pyc │ │ ├── event.cpython-39.pyc │ │ ├── webhook_model.cpython-38.pyc │ │ ├── webhook_model.cpython-39.pyc │ │ ├── webhook_validator.cpython-38.pyc │ │ └── webhook_validator.cpython-39.pyc │ ├── config.py │ ├── webhook_model.py │ └── webhook_validator.py ├── main.py ├── routers │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ ├── __init__.cpython-39.pyc │ │ ├── github.cpython-38.pyc │ │ ├── github.cpython-39.pyc │ │ ├── health_check.cpython-38.pyc │ │ └── health_check.cpython-39.pyc │ ├── github.py │ └── health_check.py └── snyk │ ├── __init__.py │ ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── __init__.cpython-39.pyc │ ├── client.cpython-38.pyc │ ├── client.cpython-39.pyc │ ├── config.cpython-38.pyc │ ├── config.cpython-39.pyc │ ├── utils.cpython-38.pyc │ └── utils.cpython-39.pyc │ ├── client.py │ ├── config.py │ └── utils.py ├── bin └── run.sh ├── docker-compose.yaml ├── requirements.txt └── tests ├── __init__.py ├── __init__.pyc ├── __pycache__ ├── __init__.cpython-36.pyc └── __init__.cpython-38.pyc ├── fixtures └── webhook_added.json ├── github ├── __init__.py ├── __init__.pyc ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── test_webhook_model.cpython-38.pyc │ └── test_webhook_validator.cpython-38.pyc ├── test_webhook_model.py ├── test_webhook_model.pyc ├── test_webhook_validator.py └── test_webhook_validator.pyc ├── requirements_test.txt └── snyk ├── __init__.py ├── __init__.pyc ├── __pycache__ ├── __init__.cpython-38.pyc ├── test_snyk_client.cpython-38.pyc └── test_snyk_utils.cpython-38.pyc ├── test_snyk_client.py ├── test_snyk_client.pyc ├── test_snyk_utils.py └── test_snyk_utils.pyc /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.0-alpine 2 | 3 | ENV APP_USER snyk_watcher 4 | ENV APP_DIR /home/$APP_USER 5 | WORKDIR $APP_DIR 6 | 7 | # Create user and group 8 | RUN addgroup -S $APP_USER && \ 9 | adduser -S -G $APP_USER -s /sbin/nologin $APP_USER 10 | 11 | RUN apk update 12 | 13 | COPY requirements.txt $APP_DIR/requirements.txt 14 | RUN apk add --virtual build-dependencies python3-dev build-base \ 15 | && pip3 install --upgrade pip \ 16 | && pip3 install -r $APP_DIR/requirements.txt \ 17 | && rm -rf /var/cache/apk/* \ 18 | && apk del build-dependencies 19 | 20 | COPY app $APP_DIR/app 21 | COPY bin $APP_DIR/bin 22 | 23 | EXPOSE 8000 24 | 25 | USER $APP_USER 26 | 27 | CMD (gunicorn app.main:app -b 0.0.0.0:8000 -w 3 -k uvicorn.workers.UvicornWorker) 28 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM python:3.9.0-alpine 2 | 3 | ENV APP_USER snyk_watcher 4 | ENV APP_DIR /home/$APP_USER 5 | WORKDIR $APP_DIR 6 | 7 | # Create user and group 8 | RUN addgroup -S $APP_USER &&\ 9 | adduser -S -G $APP_USER -s /sbin/nologin $APP_USER 10 | 11 | RUN apk update 12 | 13 | COPY requirements.txt $APP_DIR/requirements.txt 14 | COPY tests/requirements_test.txt $APP_DIR/requirements_test.txt 15 | 16 | RUN apk add --virtual build-dependencies python3-dev build-base \ 17 | && pip3 install --upgrade pip \ 18 | && pip3 install -r $APP_DIR/requirements.txt \ 19 | && pip3 install -r $APP_DIR/requirements_test.txt \ 20 | && rm -rf /var/cache/apk/* \ 21 | && apk del build-dependencies 22 | 23 | COPY app $APP_DIR/app 24 | COPY tests $APP_DIR/tests 25 | 26 | USER $APP_USER 27 | 28 | CMD (python -m pytest -v -s && pycodestyle -v app) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Twilio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ORG := security 2 | PROJECT := snyk_watcher 3 | TAG := $(REPOSITORY)/$(ORG)/$(PROJECT):latest 4 | TEST_IMAGE_NAME := $(ORG)/$(PROJECT)-test:latest 5 | TEST_ARGS:= --volume "$(CURDIR)/build":"/build" --name snyk_watcher-test $(TEST_IMAGE_NAME) 6 | 7 | DOCKER_RUN:= docker run 8 | 9 | build: 10 | docker build . --tag $(PROJECT) 11 | docker tag $(PROJECT) $(PROJECT):latest 12 | 13 | build-test: 14 | echo $(TEST_IMAGE_NAME) 15 | docker build --file Dockerfile.test . --tag $(TEST_IMAGE_NAME) 16 | 17 | test: build-test clean-test 18 | $(DOCKER_RUN) $(TEST_ARGS) 19 | 20 | serve: 21 | docker-compose up --build 22 | 23 | clean: 24 | rm -rf build 25 | 26 | clean-test: 27 | -@docker rm $(TEST_IMAGE_NAME) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## How does it work? 2 | 3 | Snyk-Watcher listens for webhooks to trigger events. It watches the main branch for the following events from Github: 4 | 5 | * Repository - Created, Deleted, Renamed 6 | * Pull Request - All 7 | 8 | For the pull request webhooks, Snyk-Watcher will only try to import a project if it has been merged to main. 9 | 10 | ## Installation 11 | 12 | Snyk-Watcher runs completely standalone in a Docker container. We've included a docker-compose file to make it easy to build. 13 | 14 | 1. Clone this repository. 15 | 16 | 1. Build the container: 17 | 18 | `docker-compose build` 19 | 20 | 1. Start Snyk-Watcher: 21 | 22 | `docker-compose up` 23 | 24 | You can verify that Snyk-Watcher is running by navigating to: 25 | 26 | `localhost:8000/healthcheck` 27 | 28 | 1. In GitHub, go to the **GitHub Apps** developer setting and click **New GitHub App**. 29 | 30 | 1. Enter the following values in the **Register new GitHub App** form: 31 | 32 | * **GitHub App name**: `Snyk-Watcher` 33 | * **Webhook URL**: {server}`/github/webhook` 34 | * **Secret**: {a highly random string} 35 | 36 | 1. Give Snyk-Watcher access to the following Repository permissions: 37 | 38 | * **Repository Administration** (Read-only) 39 | * **Metadata** (Read-only) 40 | * **Pull requests** (Read-only) 41 | 42 | 1. Subscribe to the following events: 43 | 44 | * **Pull request** 45 | * **Repository** 46 | 47 | 1. Provide the Snyk-Watcher container your GitHub and Snyk tokens in these two environment variables: 48 | 49 | * `SECRET_GITHUB_SECRET` 50 | * `SECRET_SNYK_API_TOKEN` 51 | 52 | 1. Test Snyk-Watcher by adding a new repository, then looking in the app’s advanced settings to see if the request was successful. 53 | 54 | 55 | ## Limitations 56 | 57 | * At this time, Snyk-Watcher has been tested only with GitHub and GitHub Enterprise. 58 | * Snyk-Watcher does not have access to your code and does not make any changes to your GitHub organization or repositories. 59 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__init__.py -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /app/__pycache__/api_exceptions.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/api_exceptions.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/api_exceptions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/api_exceptions.cpython-38.pyc -------------------------------------------------------------------------------- /app/__pycache__/blueprints.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/blueprints.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/blueprints.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/blueprints.cpython-38.pyc -------------------------------------------------------------------------------- /app/__pycache__/main.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/main.cpython-38.pyc -------------------------------------------------------------------------------- /app/__pycache__/main.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/main.cpython-39.pyc -------------------------------------------------------------------------------- /app/__pycache__/not_main.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/__pycache__/not_main.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__init__.py -------------------------------------------------------------------------------- /app/common/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/constants.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/constants.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/constants.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/constants.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/github_event.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/github_event.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/github_event.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/github_event.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/logging.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/logging.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/logging.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/logging.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/secrets_loader.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/secrets_loader.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/secrets_loader.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/secrets_loader.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/snyk_models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/snyk_models.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/utils.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/utils.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/utils.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/webhook_validator.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/webhook_validator.cpython-38.pyc -------------------------------------------------------------------------------- /app/common/__pycache__/webhook_validator.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/common/__pycache__/webhook_validator.cpython-39.pyc -------------------------------------------------------------------------------- /app/common/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Currently implemented actions for the below even.action pairs 4 | IMPLEMENTED_EVENTS = { 5 | "repository.created", 6 | "repository.deleted", 7 | "repository.renamed", 8 | "pull_request.closed" 9 | } 10 | 11 | # TODO: make required 12 | SNYK_INTEGRATION = os.environ.get('SNYK_INTEGRATION', 'github-enterprise') 13 | -------------------------------------------------------------------------------- /app/common/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pythonjsonlogger import jsonlogger 4 | 5 | 6 | def getLogger(name): 7 | logger = logging.getLogger(name) 8 | json_handler = logging.StreamHandler() 9 | formatter = jsonlogger.JsonFormatter( 10 | fmt='%(sid)s %(funcName)s %(levelname)s %(asctime)s %(lineno)s %(filename)s %(name)s %(message)s') # nopep8 11 | json_handler.setFormatter(formatter) 12 | logger.addHandler(json_handler) 13 | 14 | return logger 15 | -------------------------------------------------------------------------------- /app/common/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | # Get deeply nested values from JSON, only string keys allowed 5 | # @param obj - dictionary to search for path 6 | # @param path - dot separated path string 7 | def get_by_path(obj: dict, path: str, default=None) -> Any: 8 | if not obj or not path: 9 | return obj 10 | 11 | keys = path.split('.') 12 | curr = obj 13 | 14 | for key in keys: 15 | if key not in curr: 16 | return default 17 | 18 | curr = curr[key] 19 | 20 | return curr 21 | -------------------------------------------------------------------------------- /app/github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__init__.py -------------------------------------------------------------------------------- /app/github/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/config.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/config.cpython-38.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/config.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/config.cpython-39.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/event.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/event.cpython-39.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/webhook_model.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/webhook_model.cpython-38.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/webhook_model.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/webhook_model.cpython-39.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/webhook_validator.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/webhook_validator.cpython-38.pyc -------------------------------------------------------------------------------- /app/github/__pycache__/webhook_validator.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/github/__pycache__/webhook_validator.cpython-39.pyc -------------------------------------------------------------------------------- /app/github/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_github_secret(): 5 | return os.environ.get('SECRET_GITHUB_SECRET') 6 | -------------------------------------------------------------------------------- /app/github/webhook_model.py: -------------------------------------------------------------------------------- 1 | from app.common.utils import get_by_path 2 | from app.common.constants import IMPLEMENTED_EVENTS 3 | 4 | from pydantic import BaseModel, ValidationError, root_validator 5 | 6 | 7 | # Nested pull_request object, describes pull_request that triggered webhook. 8 | class PullRequest(BaseModel): 9 | # Was pull request merged 10 | merged: bool = False 11 | 12 | 13 | class Repository(BaseModel): 14 | # Organization repository belongs to 15 | org: str 16 | # Repository name 17 | name: str 18 | # default branch name 19 | default_branch: str 20 | 21 | 22 | class Changes(BaseModel): 23 | # Store repository previous name 24 | previous_name: str = None 25 | 26 | 27 | class Webhook(BaseModel): 28 | # The action that triggered the webhook from Github, required. 29 | action: str 30 | # This field only exists on pull request webhooks, optional. 31 | pull_request: PullRequest 32 | # Optional changes nested object, only exists on renamed repository event. 33 | changes: Changes 34 | # Webhook repositoy object, required. 35 | repository: Repository 36 | 37 | @root_validator(pre=True) 38 | def parse_data(cls, values: dict) -> dict: 39 | full_name = get_by_path(values, 'repository.full_name') 40 | is_merged = get_by_path(values, 'pull_request.merged', False) 41 | previous_name = get_by_path(values, 'changes.repository.name.from') 42 | default_branch = get_by_path( 43 | values, 'repository.default_branch', 'master') 44 | 45 | # full_name needs to be organization name / repository name. 46 | if '/' not in full_name: 47 | raise ValidationError('Improper full name.') 48 | 49 | org_name, repo_name = full_name.split('/') 50 | 51 | output = { 52 | 'action': values['action'], 53 | 'repository': { 54 | 'org': org_name, 55 | 'name': repo_name, 56 | 'default_branch': default_branch 57 | }, 58 | 'pull_request': { 59 | 'merged': is_merged 60 | }, 61 | 'changes': { 62 | 'previous_name': previous_name 63 | } 64 | } 65 | 66 | return output 67 | 68 | # We need to delete if repository is renamed or deleted. 69 | def requires_delete(self) -> bool: 70 | return bool(self.action == 'deleted' or self.changes.previous_name) 71 | 72 | # We need to import the repository for all events 73 | # except repository deleted. 74 | def requires_import(self) -> bool: 75 | return self.action != 'deleted' 76 | 77 | # Helper method to fetch repo to delete. 78 | def get_delete_repo(self) -> str: 79 | if self.action == 'deleted': 80 | return self.repo_name 81 | 82 | if self.changes.previous_name: 83 | return self.changes.previous_name 84 | 85 | # Ensure this webhook is for the predefined event types. 86 | def is_implemented(self, event_name: str) -> bool: 87 | full_name = f'{event_name}.{self.action}' 88 | is_merged = self.pull_request.merged 89 | 90 | # TODO: Clean up 91 | if full_name not in IMPLEMENTED_EVENTS: 92 | return False 93 | 94 | if event_name == 'pull_request' and is_merged is not True: 95 | return False 96 | 97 | return True 98 | -------------------------------------------------------------------------------- /app/github/webhook_validator.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | 4 | from app.common.logging import getLogger 5 | 6 | logger = getLogger(__name__) 7 | 8 | 9 | # Get sha1 hmac from payload and secret. 10 | def sign_payload(payload: str, secret: str) -> bool: 11 | key = bytes(secret, 'utf-8') 12 | mac = hmac.new(key, msg=payload, digestmod=hashlib.sha1) 13 | 14 | return mac.hexdigest() 15 | 16 | 17 | # Helper method to parse passed webhook signature. 18 | def extract_signature(signature: str) -> str: 19 | if not signature or 'sha1' not in signature or '=' not in signature: 20 | return '' 21 | 22 | return signature.split('=')[-1] 23 | 24 | 25 | # Compare sent hash vs calculated hash. 26 | def verify_webhook(payload: str, signature: str, secret: str) -> bool: 27 | signature_hash = extract_signature(signature) 28 | calculated_hash = sign_payload(payload, secret) 29 | 30 | if not signature_hash: 31 | logger.error('Invalid signature format provided.') 32 | return False 33 | 34 | if not hmac.compare_digest(signature_hash, calculated_hash): 35 | logger.error('Invalid signature provided.') 36 | return False 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fastapi import FastAPI 4 | from app.routers.health_check import router as health_check_router 5 | from app.routers.github import router as github_router 6 | 7 | app_config = { 8 | 'title': 'Snyk Watcher', 9 | 'description': 'A Github app to automatically sync projects to snyk', 10 | 'docs_url': None, 11 | 'redoc_url': None 12 | } 13 | 14 | # Enable swagger / redoc endpoints 15 | if os.environ.get('DEBUG') in [True, 'True', 'true']: 16 | app_config['docs_url'] = '/docs' 17 | app_config['redoc_url'] = '/redocs' 18 | 19 | 20 | app = FastAPI(**app_config) 21 | 22 | app.include_router(health_check_router) 23 | app.include_router(github_router, prefix='/github') 24 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__init__.py -------------------------------------------------------------------------------- /app/routers/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /app/routers/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /app/routers/__pycache__/github.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/github.cpython-38.pyc -------------------------------------------------------------------------------- /app/routers/__pycache__/github.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/github.cpython-39.pyc -------------------------------------------------------------------------------- /app/routers/__pycache__/health_check.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/health_check.cpython-38.pyc -------------------------------------------------------------------------------- /app/routers/__pycache__/health_check.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/routers/__pycache__/health_check.cpython-39.pyc -------------------------------------------------------------------------------- /app/routers/github.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Response, Request, Header 2 | from typing import Optional 3 | 4 | from app.common.logging import getLogger 5 | from app.github.webhook_validator import verify_webhook 6 | from app.github.webhook_model import Webhook 7 | from app.github.config import get_github_secret 8 | from app.snyk.client import SnykClient 9 | from app.snyk.utils import get_snyk_token 10 | 11 | logger = getLogger(__name__) 12 | router = APIRouter() 13 | 14 | github_secret = get_github_secret() 15 | snyk_token = get_snyk_token() 16 | 17 | 18 | @router.post('/webhook') 19 | async def handle_webhook( 20 | # Raw body is required to compute sha1 21 | request: Request, 22 | # Parsed request object 23 | webhook: Webhook, 24 | # Required header X-Hub-Signature 25 | x_hub_signature: Optional[str] = Header(...), 26 | # Required header X-Github-Event 27 | x_github_event: Optional[str] = Header(...)): 28 | # Raw body is required to compute sha1 29 | request_body = await request.body() 30 | 31 | # Verify payload sha1 to ensure request is valid 32 | if not verify_webhook(request_body, x_hub_signature, github_secret): 33 | logger.error('Webhook validation failed.') 34 | return Response(status_code=401) 35 | 36 | # Ensure this is event and action pair is implemented 37 | if not webhook.is_implemented(x_github_event): 38 | logger.error('Webhook event is not implemented.') 39 | return Response(content='Not implemented', status_code=200) 40 | 41 | # Snyk client to add / remove repositories 42 | client = SnykClient(snyk_token) 43 | 44 | org_name = webhook.repository.org 45 | repo_name = webhook.repository.name 46 | 47 | try: 48 | if webhook.requires_delete(): 49 | delete_repo = webhook.get_delete_repo() 50 | await client.delete_git_project(org_name, delete_repo) 51 | 52 | if webhook.requires_import(): 53 | await client.import_git_project(org_name, repo_name) 54 | except Exception as e: 55 | logger.error(e) 56 | # Failed internally 57 | return Response(status_code=500) 58 | 59 | # Success all done 60 | return Response(status_code=200) 61 | -------------------------------------------------------------------------------- /app/routers/health_check.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Response 2 | 3 | router = APIRouter() 4 | 5 | 6 | # Health Check 7 | @router.get('/healthcheck') 8 | async def get_health_check(): 9 | Response(status_code=200) 10 | -------------------------------------------------------------------------------- /app/snyk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__init__.py -------------------------------------------------------------------------------- /app/snyk/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/client.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/client.cpython-38.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/client.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/client.cpython-39.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/config.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/config.cpython-38.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/config.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/config.cpython-39.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/utils.cpython-38.pyc -------------------------------------------------------------------------------- /app/snyk/__pycache__/utils.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/app/snyk/__pycache__/utils.cpython-39.pyc -------------------------------------------------------------------------------- /app/snyk/client.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | 4 | from app.common.logging import getLogger 5 | from app.common.constants import SNYK_INTEGRATION 6 | from app.snyk.utils import SnykObject, parse_project_name, normalize 7 | 8 | 9 | logger = getLogger(__name__) 10 | 11 | 12 | class SnykClient: 13 | def __init__(self, api_token: str): 14 | headers = { 15 | 'Authorization': f'token {api_token}', 16 | 'Content-Type': 'application/json' 17 | } 18 | 19 | self.session = aiohttp.ClientSession(headers=headers) 20 | 21 | async def _get_org(self, org_name: str) -> SnykObject: 22 | res = await self.session.get('https://snyk.io/api/v1/orgs') 23 | 24 | if res.status == 401: 25 | raise Exception('Could not authenticate to Snyk') 26 | 27 | body = await res.json() 28 | orgs = body.get('orgs', []) 29 | 30 | if not org_name: 31 | return orgs 32 | 33 | for org in orgs: 34 | if org_name == org.get('name'): 35 | return normalize(org) 36 | 37 | raise Exception( 38 | f'Could not find {org_name}, Please ensure organizatione exists') 39 | 40 | async def get_project(self, 41 | org: SnykObject, 42 | project_name: str) -> SnykObject: 43 | 44 | url = f'https://snyk.io/api/v1/org/{org.id}/projects' 45 | res = await self.session.post(url) 46 | 47 | if res.status != 200: 48 | raise Exception(res) 49 | 50 | body = await res.json() 51 | projects = body.get('projects') 52 | 53 | for project in projects: 54 | name = project.get('name') 55 | 56 | if parse_project_name(name) == project_name: 57 | return normalize(project) 58 | 59 | return None 60 | 61 | async def get_integration(self, org: SnykObject) -> SnykObject: 62 | url = f'https://snyk.io/api/v1/org/{org.id}/integrations' 63 | res = await self.session.get(url) 64 | 65 | if res.status != 200: 66 | raise Exception(res) 67 | 68 | body = await res.json() 69 | 70 | if SNYK_INTEGRATION in body: 71 | return normalize(body, SNYK_INTEGRATION) 72 | 73 | raise Exception('Could not find integration') 74 | 75 | async def delete_git_project(self, org_name, repo_name): 76 | org = await self._get_org(org_name) 77 | project = await self.get_project(org, repo_name) 78 | 79 | if not project: 80 | return True 81 | 82 | url = f'https://snyk.io/api/v1/org/{org.id}/project/{project.id}' 83 | res = await self.session.delete(url) 84 | 85 | if res.status == 200: 86 | return True 87 | 88 | raise Exception(f'Failed to delete repository {repo_name}') 89 | 90 | async def import_git_project(self, org_name, repo_name): 91 | org = await self._get_org(org_name) 92 | project = await self.get_project(org, repo_name) 93 | 94 | if project: 95 | return 96 | 97 | data = { 98 | 'files': [], 99 | 'target': { 100 | 'owner': org_name, 101 | 'name': repo_name, 102 | 'branch': 'master' 103 | } 104 | } 105 | 106 | integration = await self.get_integration(org) 107 | url = (f'https://snyk.io/api/v1/org/{org.id}' 108 | f'/integrations/{integration.id}/import') 109 | res = await self.session.post(url, json=data) 110 | 111 | if res.status != 201: 112 | raise Exception(f'Failed to import repository {repo_name}') 113 | 114 | def __del__(self): 115 | loop = asyncio.get_running_loop() 116 | if loop and loop.is_running(): 117 | task = loop.create_task(self.session.close()) 118 | -------------------------------------------------------------------------------- /app/snyk/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_snyk_token(): 5 | return os.environ.get('SECRET_SNYK_API_TOKEN') 6 | -------------------------------------------------------------------------------- /app/snyk/utils.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from app.snyk.config import get_snyk_token 3 | 4 | 5 | SnykObject = collections.namedtuple('SnykObject', 'name id') 6 | 7 | 8 | def parse_project_name(project_name: str) -> str: 9 | name = project_name 10 | 11 | if ':' in name: 12 | name = name.split(':')[0] 13 | 14 | if '/' in name: 15 | name = name.split('/')[-1] 16 | 17 | return name 18 | 19 | 20 | def normalize(base_obj: dict, id_key: str = 'id'): 21 | return SnykObject( 22 | name=base_obj.get('name'), 23 | id=base_obj.get(id_key) 24 | ) 25 | -------------------------------------------------------------------------------- /bin/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gunicorn app.main:app -b 0.0.0.0:8000 -w 3 -k uvicorn.workers.UvicornWorker 4 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | snyk_watcher: 4 | build: . 5 | user: snyk_watcher 6 | volumes: 7 | - ./app:/home/snyk_watcher/app 8 | environment: 9 | SECRET_GITHUB_SECRET: SECRET 10 | SECRET_SNYK_API_TOKEN: SECRET 11 | command: uvicorn app.main:app --reload --host 0.0.0.0 12 | ports: 13 | - "8000:8000" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.6.2 2 | aiounittest==1.4.0 3 | fastapi==0.65.2 4 | gunicorn==20.0.4 5 | httptools==0.1.1 6 | python-json-logger==0.1.11 7 | requests==2.24.0 8 | uvicorn==0.12.1 9 | uvloop==0.14.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/__init__.py -------------------------------------------------------------------------------- /tests/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/__init__.pyc -------------------------------------------------------------------------------- /tests/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /tests/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /tests/fixtures/webhook_added.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "created", 3 | "repository": { 4 | "id": "repoid", 5 | "node_id": "nodeid", 6 | "name": "test6", 7 | "full_name": "test-onboarder/test6", 8 | "private": true, 9 | "owner": { 10 | "login": "userid", 11 | "id": "userid", 12 | "node_id": "nodeid", 13 | "avatar_url": "https://avatars1.githubusercontent.com/u/user?v=4", 14 | "gravatar_id": "", 15 | "url": "https://api.github.com/users/test-onboarder", 16 | "html_url": "https://github.com/test-onboarder", 17 | "followers_url": "https://api.github.com/users/test-onboarder/followers", 18 | "following_url": "https://api.github.com/users/test-onboarder/following{/other_user}", 19 | "gists_url": "https://api.github.com/users/test-onboarder/gists{/gist_id}", 20 | "starred_url": "https://api.github.com/users/test-onboarder/starred{/owner}{/repo}", 21 | "subscriptions_url": "https://api.github.com/users/test-onboarder/subscriptions", 22 | "organizations_url": "https://api.github.com/users/test-onboarder/orgs", 23 | "repos_url": "https://api.github.com/users/test-onboarder/repos", 24 | "events_url": "https://api.github.com/users/test-onboarder/events{/privacy}", 25 | "received_events_url": "https://api.github.com/users/test-onboarder/received_events", 26 | "type": "Organization", 27 | "site_admin": false 28 | }, 29 | "html_url": "https://github.com/test-onboarder/test6", 30 | "description": null, 31 | "fork": false, 32 | "url": "https://api.github.com/repos/test-onboarder/test6", 33 | "forks_url": "https://api.github.com/repos/test-onboarder/test6/forks", 34 | "keys_url": "https://api.github.com/repos/test-onboarder/test6/keys{/key_id}", 35 | "collaborators_url": "https://api.github.com/repos/test-onboarder/test6/collaborators{/collaborator}", 36 | "teams_url": "https://api.github.com/repos/test-onboarder/test6/teams", 37 | "hooks_url": "https://api.github.com/repos/test-onboarder/test6/hooks", 38 | "issue_events_url": "https://api.github.com/repos/test-onboarder/test6/issues/events{/number}", 39 | "events_url": "https://api.github.com/repos/test-onboarder/test6/events", 40 | "assignees_url": "https://api.github.com/repos/test-onboarder/test6/assignees{/user}", 41 | "branches_url": "https://api.github.com/repos/test-onboarder/test6/branches{/branch}", 42 | "tags_url": "https://api.github.com/repos/test-onboarder/test6/tags", 43 | "blobs_url": "https://api.github.com/repos/test-onboarder/test6/git/blobs{/sha}", 44 | "git_tags_url": "https://api.github.com/repos/test-onboarder/test6/git/tags{/sha}", 45 | "git_refs_url": "https://api.github.com/repos/test-onboarder/test6/git/refs{/sha}", 46 | "trees_url": "https://api.github.com/repos/test-onboarder/test6/git/trees{/sha}", 47 | "statuses_url": "https://api.github.com/repos/test-onboarder/test6/statuses/{sha}", 48 | "languages_url": "https://api.github.com/repos/test-onboarder/test6/languages", 49 | "stargazers_url": "https://api.github.com/repos/test-onboarder/test6/stargazers", 50 | "contributors_url": "https://api.github.com/repos/test-onboarder/test6/contributors", 51 | "subscribers_url": "https://api.github.com/repos/test-onboarder/test6/subscribers", 52 | "subscription_url": "https://api.github.com/repos/test-onboarder/test6/subscription", 53 | "commits_url": "https://api.github.com/repos/test-onboarder/test6/commits{/sha}", 54 | "git_commits_url": "https://api.github.com/repos/test-onboarder/test6/git/commits{/sha}", 55 | "comments_url": "https://api.github.com/repos/test-onboarder/test6/comments{/number}", 56 | "issue_comment_url": "https://api.github.com/repos/test-onboarder/test6/issues/comments{/number}", 57 | "contents_url": "https://api.github.com/repos/test-onboarder/test6/contents/{+path}", 58 | "compare_url": "https://api.github.com/repos/test-onboarder/test6/compare/{base}...{head}", 59 | "merges_url": "https://api.github.com/repos/test-onboarder/test6/merges", 60 | "archive_url": "https://api.github.com/repos/test-onboarder/test6/{archive_format}{/ref}", 61 | "downloads_url": "https://api.github.com/repos/test-onboarder/test6/downloads", 62 | "issues_url": "https://api.github.com/repos/test-onboarder/test6/issues{/number}", 63 | "pulls_url": "https://api.github.com/repos/test-onboarder/test6/pulls{/number}", 64 | "milestones_url": "https://api.github.com/repos/test-onboarder/test6/milestones{/number}", 65 | "notifications_url": "https://api.github.com/repos/test-onboarder/test6/notifications{?since,all,participating}", 66 | "labels_url": "https://api.github.com/repos/test-onboarder/test6/labels{/name}", 67 | "releases_url": "https://api.github.com/repos/test-onboarder/test6/releases{/id}", 68 | "deployments_url": "https://api.github.com/repos/test-onboarder/test6/deployments", 69 | "created_at": "2020-10-06T19:12:36Z", 70 | "updated_at": "2020-10-06T19:12:36Z", 71 | "pushed_at": null, 72 | "git_url": "git://github.com/test-onboarder/test6.git", 73 | "ssh_url": "git@github.com:test-onboarder/test6.git", 74 | "clone_url": "https://github.com/test-onboarder/test6.git", 75 | "svn_url": "https://github.com/test-onboarder/test6", 76 | "homepage": null, 77 | "size": 0, 78 | "stargazers_count": 0, 79 | "watchers_count": 0, 80 | "language": null, 81 | "has_issues": true, 82 | "has_projects": true, 83 | "has_downloads": true, 84 | "has_wiki": true, 85 | "has_pages": false, 86 | "forks_count": 0, 87 | "mirror_url": null, 88 | "archived": false, 89 | "disabled": false, 90 | "open_issues_count": 0, 91 | "license": null, 92 | "forks": 0, 93 | "open_issues": 0, 94 | "watchers": 0, 95 | "default_branch": "main" 96 | }, 97 | "installation": { 98 | "id": 12345, 99 | "node_id": "nodeid" 100 | } 101 | } -------------------------------------------------------------------------------- /tests/github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/__init__.py -------------------------------------------------------------------------------- /tests/github/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/__init__.pyc -------------------------------------------------------------------------------- /tests/github/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /tests/github/__pycache__/test_webhook_model.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/__pycache__/test_webhook_model.cpython-38.pyc -------------------------------------------------------------------------------- /tests/github/__pycache__/test_webhook_validator.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/__pycache__/test_webhook_validator.cpython-38.pyc -------------------------------------------------------------------------------- /tests/github/test_webhook_model.py: -------------------------------------------------------------------------------- 1 | import aiounittest 2 | import copy 3 | import pydantic 4 | 5 | from app.github.webhook_model import Webhook 6 | 7 | data = { 8 | "action": 'renamed', 9 | "pull_request": { 10 | "merged": True 11 | }, 12 | "repository": { 13 | "full_name": "organization/project", 14 | "lastName": "p", 15 | "age": 71 16 | }, 17 | "changes": { 18 | "repository": { 19 | "name": { 20 | "from": 'project2' 21 | } 22 | } 23 | } 24 | } 25 | 26 | data2 = { 27 | "action": 'created', 28 | "repository": { 29 | "full_name": "organization/project", 30 | "lastName": "p", 31 | "age": 71 32 | } 33 | } 34 | 35 | class TestWebhookValidator(aiounittest.AsyncTestCase): 36 | def test_good_import(self): 37 | local_data = copy.deepcopy(data2) 38 | event = Webhook(**local_data) 39 | 40 | self.assertTrue(event.requires_import()) 41 | self.assertEqual(False, event.requires_delete()) 42 | 43 | def test_good_delete(self): 44 | local_data = copy.deepcopy(data2) 45 | local_data['action'] = 'deleted' 46 | event = Webhook(**local_data) 47 | 48 | self.assertTrue(event.requires_delete()) 49 | self.assertEqual(False, event.requires_import()) 50 | 51 | def test_good_renamed(self): 52 | local_data = copy.deepcopy(data) 53 | event = Webhook(**local_data) 54 | 55 | self.assertTrue(event.requires_delete()) 56 | self.assertTrue(event.requires_import()) 57 | 58 | def test_repo_name(self): 59 | local_data = copy.deepcopy(data2) 60 | local_data['repository']['full_name'] = '' 61 | threw = False 62 | 63 | try: 64 | event = Webhook(**local_data) 65 | except pydantic.ValidationError as e: 66 | threw = True 67 | 68 | self.assertTrue(threw) -------------------------------------------------------------------------------- /tests/github/test_webhook_model.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/test_webhook_model.pyc -------------------------------------------------------------------------------- /tests/github/test_webhook_validator.py: -------------------------------------------------------------------------------- 1 | import aiounittest 2 | from app.github import webhook_validator 3 | 4 | secret = 'SECRET' 5 | 6 | payload_hash = '93a95e9f9dccd84f6789689e952b54a5575b1f34' 7 | 8 | sample_payload = { 9 | "action": "added", 10 | "repository": { 11 | "full_name": 'organization/project' 12 | } 13 | } 14 | 15 | class TestWebhookValidator(aiounittest.AsyncTestCase): 16 | def test_sign_payload(self): 17 | payload = str(sample_payload).encode('utf-8') 18 | signature = webhook_validator.sign_payload(payload, secret) 19 | self.assertEqual(payload_hash, signature) 20 | 21 | payload = (str(sample_payload) + '1').encode('utf-8') 22 | signature = webhook_validator.sign_payload(payload, secret) 23 | 24 | self.assertNotEqual(payload_hash, signature) 25 | 26 | 27 | def test_exctract_signature(self): 28 | """ 29 | This should fail for anything other than a sha1 30 | in the following format 31 | sha1=hash 32 | """ 33 | test = webhook_validator.extract_signature('sha1=hash') 34 | self.assertEqual('hash', test) 35 | 36 | test = webhook_validator.extract_signature('hash') 37 | self.assertEqual('', test) 38 | 39 | test = webhook_validator.extract_signature('sha=hash') 40 | self.assertEqual('', test) 41 | 42 | 43 | def test_verify_webhook(self): 44 | payload = str(sample_payload).encode('utf-8') 45 | signature = 'sha1=' + payload_hash 46 | 47 | res = webhook_validator.verify_webhook(payload, signature, secret) 48 | self.assertEqual(res, True) 49 | 50 | res = webhook_validator.verify_webhook(payload, payload_hash, secret) 51 | self.assertEqual(res, False) 52 | 53 | res = webhook_validator.verify_webhook(payload, signature, 'WRONG_SECRET') 54 | self.assertEqual(res, False) 55 | -------------------------------------------------------------------------------- /tests/github/test_webhook_validator.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/github/test_webhook_validator.pyc -------------------------------------------------------------------------------- /tests/requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pycodestyle -------------------------------------------------------------------------------- /tests/snyk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/__init__.py -------------------------------------------------------------------------------- /tests/snyk/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/__init__.pyc -------------------------------------------------------------------------------- /tests/snyk/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /tests/snyk/__pycache__/test_snyk_client.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/__pycache__/test_snyk_client.cpython-38.pyc -------------------------------------------------------------------------------- /tests/snyk/__pycache__/test_snyk_utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/__pycache__/test_snyk_utils.cpython-38.pyc -------------------------------------------------------------------------------- /tests/snyk/test_snyk_client.py: -------------------------------------------------------------------------------- 1 | import aiounittest 2 | from unittest import mock 3 | 4 | from app.github import webhook_validator 5 | from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop 6 | 7 | secret = 'SECRET' 8 | 9 | payload_hash = '93a95e9f9dccd84f6789689e952b54a5575b1f34' 10 | 11 | sample_payload = { 12 | "action": "added", 13 | "repository": { 14 | "full_name": 'organization/project' 15 | } 16 | } 17 | 18 | class TestWebhookValidator(aiounittest.AsyncTestCase): 19 | def test_sign_payload(self): 20 | payload = str(sample_payload).encode('utf-8') 21 | signature = webhook_validator.sign_payload(payload, secret) 22 | self.assertEqual(payload_hash, signature) 23 | 24 | payload = (str(sample_payload) + '1').encode('utf-8') 25 | signature = webhook_validator.sign_payload(payload, secret) 26 | 27 | self.assertNotEqual(payload_hash, signature) 28 | 29 | 30 | def test_exctract_signature(self): 31 | """ 32 | This should fail for anything other than a sha1 33 | in the following format 34 | sha1=hash 35 | """ 36 | test = webhook_validator.extract_signature('sha1=hash') 37 | self.assertEqual('hash', test) 38 | 39 | test = webhook_validator.extract_signature('hash') 40 | self.assertEqual('', test) 41 | 42 | test = webhook_validator.extract_signature('sha=hash') 43 | self.assertEqual('', test) 44 | 45 | 46 | def test_verify_webhook(self): 47 | payload = str(sample_payload).encode('utf-8') 48 | signature = 'sha1=' + payload_hash 49 | 50 | res = webhook_validator.verify_webhook(payload, signature, secret) 51 | self.assertEqual(res, True) 52 | 53 | res = webhook_validator.verify_webhook(payload, payload_hash, secret) 54 | self.assertEqual(res, False) 55 | 56 | res = webhook_validator.verify_webhook(payload, signature, 'WRONG_SECRET') 57 | self.assertEqual(res, False) 58 | -------------------------------------------------------------------------------- /tests/snyk/test_snyk_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/test_snyk_client.pyc -------------------------------------------------------------------------------- /tests/snyk/test_snyk_utils.py: -------------------------------------------------------------------------------- 1 | import aiounittest 2 | from unittest import mock 3 | 4 | from app.snyk import utils 5 | 6 | 7 | 8 | class TestSnykUtils(aiounittest.AsyncTestCase): 9 | def test_parse_project_name(self): 10 | project_name = 'org/project' 11 | res = utils.parse_project_name(project_name) 12 | self.assertEqual('project', res) 13 | 14 | project_name = 'org/_project' 15 | res = utils.parse_project_name(project_name) 16 | self.assertEqual('_project', res) 17 | 18 | project_name = 'org/_project:latest' 19 | res = utils.parse_project_name(project_name) 20 | self.assertEqual('_project', res) 21 | 22 | project_name = 'project:latest' 23 | res = utils.parse_project_name(project_name) 24 | self.assertEqual('project', res) 25 | 26 | 27 | def test_normalize_snyk_response(self): 28 | sample_data = { 29 | 'name': 'test_name', 30 | 'id': 'test_id' 31 | } 32 | 33 | res = utils.normalize(sample_data) 34 | self.assertEqual(res.name, 'test_name') 35 | self.assertEqual(res.id, 'test_id') 36 | 37 | sample_data = { 38 | 'name': 'test_name', 39 | 'id2': 'test_id' 40 | } 41 | 42 | res = utils.normalize(sample_data, 'id2') 43 | self.assertEqual(res.name, 'test_name') 44 | self.assertEqual(res.id, 'test_id') 45 | -------------------------------------------------------------------------------- /tests/snyk/test_snyk_utils.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/snyk-watcher/e9dc94389e0aaaba3d2b6b3a03b23d5224aabfa9/tests/snyk/test_snyk_utils.pyc --------------------------------------------------------------------------------