Skip to content

Commit

Permalink
Adds github client and PAT inspection (#138)
Browse files Browse the repository at this point in the history
* Adds github client and PAT inspection

* CI, Python version fixes

Consolidate exceptions

Signed-off-by: Caroline Russell <caroline@appthreat.dev>

Output exception

Signed-off-by: Caroline Russell <caroline@appthreat.dev>

Handle GitHubException

Signed-off-by: Caroline Russell <caroline@appthreat.dev>

Don't fail-fast version_tests2

Signed-off-by: Caroline Russell <caroline@appthreat.dev>

Fix type hints, unnecessary test runs

Signed-off-by: Caroline Russell <caroline@appthreat.dev>

* Addressing PR concerns

* Adds 3.12 tests (#137)

* Adds 3.12 tests

Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>

* Adds 3.12 tests

Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>

---------

Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>

* Update to work with GitHub Actions tokens

---------

Signed-off-by: Caroline Russell <caroline@appthreat.dev>
Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>
Co-authored-by: Tim Messing <141575989+timmyteo@users.noreply.github.com>
Co-authored-by: Caroline Russell <caroline@appthreat.dev>
Co-authored-by: prabhu <prabhu@appthreat.com>
  • Loading branch information
3 people committed Oct 25, 2023
1 parent 4910126 commit 54dd761
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/dockertests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
- feature/*
paths-ignore:
- '**/README.md'
- 'gobintests.yml'
- 'pythonapp.yml'
- 'python-publish.yml'
pull_request:
jobs:
ubuntu_version_tests:
Expand Down Expand Up @@ -58,6 +61,7 @@ jobs:
matrix:
os: [ubuntu-latest]
python-version: ['3.11']
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/gobintests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ on:
- master
paths-ignore:
- '**/README.md'
- 'dockertests.yml'
- 'pythonapp.yml'
- 'python-publish.yml'
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
paths-ignore:
- '**/README.md'
- 'dockertests.yml'
- 'dockertests.yml'
- 'python-publish.yml'
pull_request:
jobs:
build:
Expand All @@ -12,6 +15,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
push:
paths-ignore:
- '**/README.md'
- 'dockertests.yml'
- 'pythonapp.yml'
- 'gobintests.yml'
branches:
- master
tags:
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,10 @@ The following environment variables can be used to customise the behaviour.
## GitHub Security Advisory
To download security advisories from GitHub, a personal access token with the following scope is necessary.
To download security advisories from GitHub, a personal access token with minimal permissions is necessary.
- read:packages
- Fine-grained token: Grant no permissions and select the following for repository access: `Public Repositories (read-only)`
- Token (classic): Grant no permissions
```bash
export GITHUB_TOKEN="<PAT token>"
Expand Down
17 changes: 15 additions & 2 deletions depscan/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import oras.client

from depscan.lib import privado, utils, github
from depscan.lib.csaf import export_csaf, write_toml
from depscan.lib import privado, utils
from depscan.lib.analysis import (
Expand Down Expand Up @@ -791,8 +792,20 @@ def main():
)

sources_list = [OSVSource(), NvdSource()]
if os.environ.get("GITHUB_TOKEN"):
sources_list.insert(0, GitHubSource())
github_token = os.environ.get("GITHUB_TOKEN")
if github_token:
github_client = github.GitHub(github_token)

if not github_client.can_authenticate():
LOG.error("The GitHub personal access token supplied appears to be invalid or expired. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory")
else:
sources_list.insert(0, GitHubSource())
scopes = github_client.get_token_scopes()
if not scopes is None and len(scopes) > 0:
LOG.warning(
"The GitHub personal access token was granted more permissions than is necessary for depscan to operate, including the scopes of: %s. It is recommended to use a dedicated token with only the minimum scope necesary for depscan to operate. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory",
', '.join([scope for scope in scopes])
)
if run_cacher:
LOG.debug(
"About to download vdb from %s. This might take a while ...",
Expand Down
62 changes: 62 additions & 0 deletions depscan/lib/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from github import Github, Auth
from depscan.lib import config
import httpx


class GitHub:
# The GitHub instance object from the PyGithub library
github = None
github_token = None


def __init__(self, github_token: str) -> None:
self.github = Github(auth=Auth.Token(github_token))
self.github_token = github_token


def can_authenticate(self) -> bool:
"""
Calls the GitHub API to determine if the token is valid
:return: Flag indicating whether authentication was successful or not
"""
headers = {"Authorization": f"token {self.github_token}"}

response = httpx.get(
url='https://api.github.com/',
headers=headers,
follow_redirects=True,
timeout=config.request_timeout_sec
)

if response.status_code == 401:
return False
else:
return True


def get_token_scopes(self) -> list:
"""
Provides the scopes associated to the access token provided in the environment variable
Only classic personal access tokens will result in scopes returned from the GitHub API
:return: List of token scopes
"""
headers = {"Authorization": f"token {self.github_token}"}

response = httpx.get(
url='https://api.github.com/',
headers=headers,
follow_redirects=True,
timeout=config.request_timeout_sec
)

oauth_scopes = response.headers.get('x-oauth-scopes')

if not oauth_scopes is None:
if oauth_scopes == '':
return None
else:
return oauth_scopes.split(', ')

return None
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"PyYAML",
"rich",
"quart",
"PyGithub",
"toml",
]

Expand Down Expand Up @@ -44,7 +45,8 @@ scan = "depscan.cli:main"
dev = ["black",
"flake8",
"pytest",
"pytest-cov"
"pytest-cov",
"httpretty"
]

[build-system]
Expand Down
122 changes: 122 additions & 0 deletions test/test_github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from depscan.lib import github
import httpretty


url = 'https://api.github.com/'


def test_can_authenticate_success():
httpretty.enable()
httpretty.reset()

headers = {
'content-type': 'application/json',
'X-OAuth-Scopes': 'admin:org, admin:repo_hook, repo, user',
'X-Accepted-OAuth-Scopes': 'repo'
}

httpretty.register_uri(
method=httpretty.GET,
uri=url,
adding_headers=headers
)

github_client = github.GitHub('test-token')
result = github_client.can_authenticate()

httpretty.disable()

assert result == True


def test_can_authenticate_unauthentiated():
httpretty.enable()
httpretty.reset()

headers = {
'content-type': 'application/json'
}

httpretty.register_uri(
method=httpretty.GET,
uri=url,
body='{"message":"Bad credentials"}',
adding_headers=headers,
status=401
)

github_client = github.GitHub('test-token')
result = github_client.can_authenticate()

httpretty.disable()

assert result == False


def test_get_token_scopes_success():
httpretty.enable()
httpretty.reset()

headers = {
'content-type': 'application/json',
'X-OAuth-Scopes': 'admin:org, admin:repo_hook, repo, user',
'X-Accepted-OAuth-Scopes': 'repo'
}

httpretty.register_uri(
method=httpretty.GET,
uri=url,
adding_headers=headers
)

github_client = github.GitHub('test-token')
result = github_client.get_token_scopes()

httpretty.disable()

assert len(result) == 4 and result.index('admin:org') >= 0 and result.index('admin:repo_hook') >= 0 and result.index('repo') >= 0 and result.index('user') >= 0


def test_get_token_scopes_none():
httpretty.enable()
httpretty.reset()

headers = {
'content-type': 'application/json',
}

httpretty.register_uri(
method=httpretty.GET,
uri=url,
adding_headers=headers
)

github_client = github.GitHub('test-token')
result = github_client.get_token_scopes()

httpretty.disable()

assert result is None


def test_get_token_scopes_empty():
httpretty.enable()
httpretty.reset()

headers = {
'content-type': 'application/json',
'x-oauth-scopes': ''
}

httpretty.register_uri(
method=httpretty.GET,
uri=url,
adding_headers=headers
)

github_client = github.GitHub('test-token')
result = github_client.get_token_scopes()

httpretty.disable()

assert result is None

0 comments on commit 54dd761

Please sign in to comment.