Skip to content

Commit a12c65e

Browse files
Merge pull request #1 from mam-dev/initial-code
Initial code
2 parents 0d123f7 + 7c8ba3d commit a12c65e

File tree

14 files changed

+1542
-0
lines changed

14 files changed

+1542
-0
lines changed

.flake8

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[flake8]
2+
max-line-length = 88
3+
extend-ignore = E203
4+
extend-exclude =
5+
.tox,
6+
build,
7+
venv

.github/workflows/ci.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
matrix:
13+
python_version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
14+
runs-on: "ubuntu-latest"
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Set up Python ${{ matrix.python_version }}
18+
uses: actions/setup-python@v2
19+
with:
20+
python-version: ${{ matrix.python_version }}
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install tox
25+
- name: Run tox
26+
run: tox -e py$(sed 's/\.//' ${{ matrix.python_version }})

.github/workflows/publish.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Thanks to: Sean Hammond
2+
# https://www.seanh.cc/2022/05/21/publishing-python-packages-from-github-actions
3+
name: Publish to PyPI.org
4+
on:
5+
release:
6+
types: [published]
7+
jobs:
8+
pypi:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v3
13+
with:
14+
fetch-depth: 0
15+
- run: python3 -m pip install --upgrade build && python3 -m build
16+
- name: Publish package
17+
uses: pypa/gh-action-pypi-publish@release/v1
18+
with:
19+
password: ${{ secrets.PYPI_API_TOKEN }}

README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# security-constraints
2+
3+
Security-constraints is a command-line application used
4+
to fetch security vulnerabilities in Python packages from
5+
external sources and from them generate version constraints
6+
for the packages.
7+
8+
The constraints can then be given to `pip install` with the `-c` option,
9+
either on the command line or in a requirements file.
10+
11+
## Installation
12+
13+
Just install it with `pip`:
14+
```bash
15+
pip install security-constraints
16+
```
17+
18+
## Usage
19+
20+
The environment variable `SC_GITHUB_TOKEN` needs to be set
21+
to a valid GitHub token which provides read access to public
22+
repositories. This is needed in order to access GitHub Security
23+
Advisory. Once this is set, you can simply run the program to
24+
output safe pip constraints to stdout.
25+
26+
```bash
27+
>security-constraints
28+
# Generated by security-constraints on 2022-11-04T08:33:54.523625
29+
# Data sources: Github Security Advisory
30+
# Configuration: {'ignore_ids': []}
31+
...
32+
vncauthproxy<0,>=1.2.0 # CVE-2022-36436 (ID: GHSA-237r-mx84-7x8c)
33+
waitress!=1.4.2 # CVE-2020-5236 (ID: GHSA-73m2-3pwg-5fgc)
34+
waitress>=1.4.0 # GHSA-4ppp-gpcr-7qf6 (ID: GHSA-4ppp-gpcr-7qf6)
35+
ymlref>0.1.1 # CVE-2018-20133 (ID: GHSA-8r8j-xvfj-36f9)
36+
>
37+
```
38+
39+
You can use `--output` to instead output to a file.
40+
41+
```bash
42+
>security-constraints --output constraints.txt
43+
>cat constraints.txt
44+
# Generated by security-constraints on 2022-11-04T08:33:54.523625
45+
# Data sources: Github Security Advisory
46+
# Configuration: {'ignore_ids': []}
47+
...
48+
vncauthproxy<0,>=1.2.0 # CVE-2022-36436 (ID: GHSA-237r-mx84-7x8c)
49+
waitress!=1.4.2 # CVE-2020-5236 (ID: GHSA-73m2-3pwg-5fgc)
50+
waitress>=1.4.0 # GHSA-4ppp-gpcr-7qf6 (ID: GHSA-4ppp-gpcr-7qf6)
51+
ymlref>0.1.1 # CVE-2018-20133 (ID: GHSA-8r8j-xvfj-36f9)
52+
>
53+
```
54+
55+
You can provide a space-separated list of IDs of vulnerabilities that
56+
should be ignored. The IDs in question are those that appear in after
57+
`ID:` in the comments in the output.
58+
59+
```bash
60+
>security-constraints --ignore-ids GHSA-4ppp-gpcr-7qf6 GHSA-8r8j-xvfj-36f9
61+
# Generated by security-constraints on 2022-11-04T08:33:54.523625
62+
# Data sources: Github Security Advisory
63+
# Configuration: {'ignore_ids': ['GHSA-4ppp-gpcr-7qf6', 'GHSA-8r8j-xvfj-36f9']}
64+
...
65+
vncauthproxy<0,>=1.2.0 # CVE-2022-36436 (ID: GHSA-237r-mx84-7x8c)
66+
waitress!=1.4.2 # CVE-2020-5236 (ID: GHSA-73m2-3pwg-5fgc)
67+
>
68+
```
69+
70+
The IDs to ignore can also be given in a configuration file using `--config`.
71+
To create an initial configuration file, you can use `--dump-config`. This
72+
will dump the current configuration (including any `--ignore-ids` passed) to
73+
stdout and then exit. You can redirect this into a file to create an
74+
initial configuration file. The configuration file is in yaml format.
75+
76+
```bash
77+
>security-constraints --ignore-ids GHSA-4ppp-gpcr-7qf6 GHSA-8r8j-xvfj-36f9 --dump-config > sc_config.yaml
78+
>cat sc_config.yaml
79+
ignore_ids:
80+
- GHSA-4ppp-gpcr-7qf6
81+
- GHSA-8r8j-xvfj-36f9
82+
>security-constraints --config sc_config.yaml
83+
# Generated by security-constraints on 2022-11-04T08:33:54.523625
84+
# Data sources: Github Security Advisory
85+
# Configuration: {'ignore_ids': ['GHSA-4ppp-gpcr-7qf6', 'GHSA-8r8j-xvfj-36f9']}
86+
...
87+
vncauthproxy<0,>=1.2.0 # CVE-2022-36436 (ID: GHSA-237r-mx84-7x8c)
88+
waitress!=1.4.2 # CVE-2020-5236 (ID: GHSA-73m2-3pwg-5fgc)
89+
>
90+
```
91+
92+
## Contributing
93+
Pull requests as well as new issues are welcome.
94+
95+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
96+
![example workflow](https://github.com/mam-dev/security-constraints/actions/workflows/ci.yml/badge.svg)

pyproject.toml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
[project]
2+
name = "security-constraints"
3+
version = "1.0.0"
4+
description = "Fetches security vulnerabilities and creates pip-constraints based on them."
5+
readme = "README.md"
6+
license = {file = "LICENSE"}
7+
requires-python = ">=3.7"
8+
dependencies = [
9+
"requests",
10+
"pyyaml",
11+
"importlib-metadata >= 1.0 ; python_version < '3.8'"
12+
]
13+
14+
[project.optional-dependencies]
15+
test = [
16+
"pytest",
17+
"requests-mock",
18+
"freezegun"
19+
]
20+
lint = [
21+
"isort",
22+
"black",
23+
"flake8",
24+
"mypy",
25+
"types-requests",
26+
"types-PyYAML"
27+
]
28+
29+
[project.scripts]
30+
security-constraints = "security_constraints.main:main"
31+
32+
[build-system]
33+
requires = ["setuptools>=51", "wheel", "setuptools_scm[toml]>=6.2"]
34+
build-backend = "setuptools.build_meta"
35+
36+
[tool.setuptools_scm]
37+
38+
[tool.setuptools.packages.find]
39+
where = ["src"]
40+
namespaces = false
41+
42+
[tool.isort]
43+
profile = "black"
44+
src_paths = ["src", "test"]
45+
46+
[tool.pytest.ini_options]
47+
minversion = "6.0"
48+
usefixtures = ["requests_mock"]
49+
testpaths = ["test"]
50+
51+
[tool.mypy]
52+
warn_return_any = true
53+
warn_unused_configs = true
54+
warn_unused_ignores = true
55+
warn_redundant_casts = true
56+
warn_unreachable = true
57+
files = ["src", "test"]
58+
59+
[[tool.mypy.overrides]]
60+
module = 'py'
61+
ignore_missing_imports = true

src/security_constraints/__init__.py

Whitespace-only changes.

src/security_constraints/common.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""This module contains common definitions for use in any other module."""
2+
import abc
3+
import dataclasses
4+
from typing import Dict, List
5+
6+
7+
class SecurityConstraintsError(Exception):
8+
"""Base class for all exceptions in this application."""
9+
10+
11+
class FailedPrerequisitesError(SecurityConstraintsError):
12+
"""Error raised when something is missing in order to run the application."""
13+
14+
15+
class FetchVulnerabilitiesError(SecurityConstraintsError):
16+
"""Error which occurred when fetching vulnerabilities."""
17+
18+
19+
@dataclasses.dataclass
20+
class Configuration:
21+
"""The application configuration.
22+
23+
Corresponds to the contents of a configuration file.
24+
25+
"""
26+
27+
ignore_ids: List[str] = dataclasses.field(default_factory=list)
28+
29+
def to_dict(self) -> Dict:
30+
return dataclasses.asdict(self)
31+
32+
@classmethod
33+
def from_dict(cls, json: Dict) -> "Configuration":
34+
return cls(**json)
35+
36+
@classmethod
37+
def supported_keys(cls) -> List[str]:
38+
"""Return a list of keys which are supported in the config file."""
39+
return list(cls().to_dict().keys())
40+
41+
42+
@dataclasses.dataclass
43+
class PackageConstraints:
44+
"""Version constraints for a single python package.
45+
46+
Attributes:
47+
package: The name of the package.
48+
specifies: A list of version specifiers, e.g. ">3.0".
49+
50+
"""
51+
52+
package: str
53+
specifiers: List[str] = dataclasses.field(default_factory=list)
54+
55+
def __str__(self) -> str:
56+
return f"{self.package}{','.join(self.specifiers)}"
57+
58+
59+
@dataclasses.dataclass
60+
class SecurityVulnerability:
61+
"""A security vulnerability in a Python package.
62+
63+
Attributes:
64+
name: Human-readable name of the vulnerability.
65+
identifier: Used to uniquely identify this vulnerability,
66+
e.g. when ignoring it.
67+
package: The name of the affected Python package.
68+
vulnerable_range: String specifying which versions are vulnerable.
69+
Syntax:
70+
= 0.2.0 denotes a single vulnerable version.
71+
<= 1.0.8 denotes a version range up to and including the specified version
72+
< 0.1.11 denotes a version range up to, but excluding, the specified version
73+
>= 4.3.0, < 4.3.5 denotes a version range with a known min and max version.
74+
>= 0.0.1 denotes a version range with a known minimum, but no known maximum.
75+
76+
"""
77+
78+
name: str
79+
identifier: str
80+
package: str
81+
vulnerable_range: str
82+
83+
def __str__(self) -> str:
84+
return self.name
85+
86+
87+
class SecurityVulnerabilityDatabaseAPI(abc.ABC):
88+
"""An API toward a database of security vulnerabilities in Python packages."""
89+
90+
@abc.abstractmethod
91+
def get_database_name(self) -> str:
92+
"""Return the name of the vulnerability database in human-readable text."""
93+
94+
@abc.abstractmethod
95+
def get_vulnerabilities(self) -> List[SecurityVulnerability]:
96+
"""Fetch and return all relevant security vulnerabilities from the database."""

0 commit comments

Comments
 (0)