Skip to content

Commit

Permalink
Add recent commits locker integrity checks (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
alfinkel authored Aug 14, 2020
1 parent 03d0a75 commit 3270484
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 25 deletions.
5 changes: 3 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# UNRELEASED
# 0.4.0

- [NEW] Add Github repository metadata fetcher.
- [NEW] Add Github repository recent commits fetcher.
- [NEW] Add Github repository branch protection fetcher.
- [NEW] Add Evidence locker integrity checks.
- [NEW] Add Evidence locker repository integrity checks.
- [NEW] Add Evidence locker recent commits integrity checks.

# 0.3.0

Expand Down
2 changes: 1 addition & 1 deletion arboretum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# limitations under the License.
"""Arboretum - Checking your compliance & security posture, continuously."""

__version__ = '0.3.0'
__version__ = '0.4.0'
65 changes: 59 additions & 6 deletions arboretum/auditree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,13 @@ used are not at the current release version.
* Behavior: For every evidence locker specified, a comparison between the current
and most recent evidence locker repository metadata is performed. If changes are
found a failure is generated by the check. A failure is also generated if
branch protection is disabled for repository administrators. Finally, a warning
is generated if the evidence locker size has shrunk when comparing the current
and most recent evidence locker repository metadata. This check can be optionally
configured to check multiple evidence lockers. If no configuration is provided
the check defaults to the repository configuration, if provided. Otherwise, the
check defaults to running against the current evidence locker URL.
branch protection is disabled for repository administrators on branches specified.
Finally, a warning is generated if the evidence locker size has shrunk when comparing
the current and most recent evidence locker repository metadata. This check can
be optionally configured to check multiple evidence lockers and branches. If
no configuration is provided the check defaults to the repository configuration,
if provided. Otherwise, the check defaults to running against the current evidence
locker URL.
* Evidence depended upon:
* Evidence locker(s) metadata and branch protection evidence.
* `raw/auditree/<gh|gl|bb>_<org>_<repo>_repo_metadata.json`
Expand Down Expand Up @@ -465,6 +466,57 @@ check defaults to running against the current evidence locker URL.
from arboretum.auditree.checks.test_locker_repo_integrity import LockerRepoIntegrityCheck
```

### Evidence Locker Commit Integrity

* Class: [LockerCommitIntegrityCheck][check-locker-commit-integrity]
* Purpose: Ensure that evidence locker(s) commits are signed.
* Behavior: For every evidence locker and branch specified, commits are checked
for a verified signature and branch protection is also checked to ensure that
commit signatures are required. Failures are generated for each violation found.
This check can be optionally configured to check multiple evidence lockers and
branches. If no configuration is provided the check defaults to the repository
configuration, if provided. Otherwise, the check defaults to running against
the current evidence locker URL.
* Evidence depended upon:
* Evidence locker(s) recent commits and branch protection evidence.
* `raw/auditree/<gh|gl|bb>_<org>_<repo>_<branch>_recent_commits.json`
* `raw/auditree/<gh|gl|bb>_<org>_<repo>_<branch>_branch_protection.json`
* Gathered by the `auditree` [GithubRepoCommitsFetcher][fetch-recent-commits]
and the `auditree` [GithubRepoBranchProtectionFetcher][fetch-branch-protection]
* NOTE: Only `gh` (Github) is currently supported by this check. Gitlab
and Bitbucket support coming soon...
* Configuration elements:
* `org.auditree.locker_integrity.branches`
* Optional
* Dictionary:
* Key: Github repository URL (string)
* Value: List of branches (string) for that repository.
* Use if looking to specify multiple repos/branches or to override either the
`repo_integrity` config or the the evidence locker repo and `master` branch
default. Otherwise do not include.
* Example (optional) configuration:

```json
{
"org": {
"auditree": {
"locker_integrity": {
"branches": {
"https://github.com/org-foo/repo-foo": ["main", "develop"],
"https://github.com/org-bar/repo-bar": ["main"]
}
}
}
}
}
```

* Import statement:

```python
from arboretum.auditree.checks.test_locker_commit_integrity import LockerCommitIntegrityCheck
```

[usage]: https://github.com/ComplianceAsCode/auditree-arboretum#usage
[fetch-abandoned-evidence]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/fetchers/fetch_abandoned_evidence.py
[fetch-compliance-config]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/fetchers/fetch_compliance_config.py
Expand All @@ -476,3 +528,4 @@ check defaults to running against the current evidence locker URL.
[check-compliance-config]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/checks/test_compliance_config.py
[check-python-packages]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/checks/test_python_packages.py
[check-locker-integrity]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/checks/test_locker_repo_integrity.py
[check-locker-commit-integrity]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/auditree/checks/test_locker_commit_integrity.py
151 changes: 151 additions & 0 deletions arboretum/auditree/checks/test_locker_commit_integrity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# -*- mode:python; coding:utf-8 -*-
# Copyright (c) 2020 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Evidence locker commit integrity checks."""

from urllib.parse import urlparse

from arboretum.auditree.evidences.repo_branch_protection import (
RepoBranchProtectionEvidence
)
from arboretum.auditree.evidences.repo_commit import RepoCommitEvidence

from compliance.check import ComplianceCheck
from compliance.evidence import DAY, ReportEvidence, evidences


class LockerCommitIntegrityCheck(ComplianceCheck):
"""Monitor the integrity of evidence locker repository commits."""

@property
def title(self):
"""
Return the title of the checks.
:returns: the title of the checks
"""
return 'Locker Commit Integrity'

@classmethod
def setUpClass(cls):
"""Initialize the check object with configuration settings."""
cls.config.add_evidences(
[
ReportEvidence(
'locker_commit_integrity.md',
'auditree',
DAY,
'Evidence locker commit integrity report.'
)
]
)
return cls

def test_recent_commit_integrity(self):
"""Check that recent commits are signed."""
locker_branches = self.config.get(
'org.auditree.locker_integrity.branches',
self.config.get(
'org.auditree.repo_integrity.branches',
{self.config.get('locker.repo_url'): ['master']}
)
)
for locker_url, branches in locker_branches.items():
parsed = urlparse(locker_url)
service = 'gh'
if 'gitlab' in parsed.hostname:
service = 'gl'
elif 'bitbucket' in parsed.hostname:
service = 'bb'
repo = parsed.path.strip('/')
for branch in branches:
filename = [
service,
repo.lower().replace('/', '_').replace('-', '_'),
branch.lower().replace('-', '_'),
'recent_commits.json'
]
path = f'raw/auditree/{"_".join(filename)}'
with evidences(self, path) as raw:
evidence = RepoCommitEvidence.from_evidence(raw)
for commit in evidence.commit_signed_status:
if not commit['signed']:
self.add_failures(
'Locker Recent Commits - (Unsigned)',
(
f'[{commit["sha"][:8]}]({commit["url"]}) '
f'commit in `{locker_url}` '
f'`{branch}` branch.'
)
)

def test_branch_protection_commit_integrity(self):
"""Check that branch protection requires signed commits."""
locker_branches = self.config.get(
'org.auditree.locker_integrity.branches',
self.config.get(
'org.auditree.repo_integrity.branches',
{self.config.get('locker.repo_url'): ['master']}
)
)
for locker_url, branches in locker_branches.items():
parsed = urlparse(locker_url)
service = 'gh'
if 'gitlab' in parsed.hostname:
service = 'gl'
elif 'bitbucket' in parsed.hostname:
service = 'bb'
repo = parsed.path.strip('/')
for branch in branches:
filename = [
service,
repo.lower().replace('/', '_').replace('-', '_'),
branch.lower().replace('-', '_'),
'branch_protection.json'
]
path = f'raw/auditree/{"_".join(filename)}'
with evidences(self, path) as raw:
evidence = RepoBranchProtectionEvidence.from_evidence(raw)
if not evidence.signed_commits_required:
self.add_failures(
(
'Locker Branch Protection - '
'(Signed Commits Disabled)'
),
f'`{locker_url}` `{branch}` branch.'
)

def get_reports(self):
"""
Provide the check report name.
:returns: the report(s) generated for this check
"""
return ['auditree/locker_commit_integrity.md']

def msg_recent_commit_integrity(self):
"""
Evidence locker recent commits integrity check notifier.
:returns: notification dictionary
"""
return {'subtitle': 'Locker signed commits', 'body': None}

def msg_branch_protection_commit_integrity(self):
"""
Evidence locker branch protection commit integrity check notifier.
:returns: notification dictionary
"""
return {'subtitle': 'Locker require commit signatures', 'body': None}
19 changes: 12 additions & 7 deletions arboretum/auditree/checks/test_locker_repo_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ def test_metadata_integrity(self):
elif 'bitbucket' in parsed.hostname:
service = 'bb'
repo = parsed.path.strip('/')
name_prefix = repo.lower().replace('/', '_').replace('-', '_')
path = f'raw/auditree/{service}_{name_prefix}_repo_metadata.json'
filename = [
service,
repo.lower().replace('/', '_').replace('-', '_'),
'repo_metadata.json'
]
path = f'raw/auditree/{"_".join(filename)}'
with evidences(self, path) as raw:
evidence_found = True
previous_dt = datetime.utcnow() - timedelta(days=1)
Expand Down Expand Up @@ -140,13 +144,14 @@ def test_branch_protection_integrity(self):
service = 'bb'
repo = parsed.path.strip('/')
for branch in branches:
name_prefix_parts = [
filename = [
service,
repo.lower().replace('/', '_').replace('-', '_'),
branch.lower().replace('-', '_')
branch.lower().replace('-', '_'),
'branch_protection.json'
]
name_prefix = '_'.join(name_prefix_parts)
filename = f'{service}_{name_prefix}_branch_protection.json'
with evidences(self, f'raw/auditree/{filename}') as raw:
path = f'raw/auditree/{"_".join(filename)}'
with evidences(self, path) as raw:
evidence = RepoBranchProtectionEvidence.from_evidence(raw)
if not evidence.admin_enforce:
self.add_failures(
Expand Down
36 changes: 33 additions & 3 deletions arboretum/auditree/evidences/repo_branch_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class RepoBranchProtectionEvidence(RawEvidence):

@property
def admin_enforce(self):
"""Provide branch protection status for admins as a property."""
"""Provide branch protection enforcement status for admins."""
if self.content:
ae_factory = {
'gh': self._get_gh_admin_enforce,
Expand All @@ -35,12 +35,42 @@ def admin_enforce(self):
self._admin_enforce = ae_factory[self.name[:2]]()
return self._admin_enforce

@property
def signed_commits_required(self):
"""Provide signed commits requirement status."""
if self.content:
sc_factory = {
'gh': self._get_gh_signed_commits_required,
'gl': self._get_gl_signed_commits_required,
'bb': self._get_bb_signed_commits_required
}
if not hasattr(self, '_signed_commits_required'):
self._signed_commits_required = sc_factory[self.name[:2]]()
return self._signed_commits_required

@property
def as_a_dict(self):
"""Provide branch protection content as a dictionary."""
if self.content:
if not hasattr(self, '_as_a_dict'):
self._as_a_dict = json.loads(self.content)
return self._as_a_dict

def _get_gh_admin_enforce(self):
as_dict = json.loads(self.content)
return as_dict.get('enforce_admins', {}).get('enabled', False)
return self.as_a_dict.get('enforce_admins', {}).get('enabled', False)

def _get_gl_admin_enforce(self):
raise NotImplementedError('Support for Gitlab coming soon...')

def _get_bb_admin_enforce(self):
raise NotImplementedError('Support for Bitbucket coming soon...')

def _get_gh_signed_commits_required(self):
sigs = self.as_a_dict.get('required_signatures', {})
return sigs.get('enabled', False)

def _get_gl_signed_commits_required(self):
raise NotImplementedError('Support for Gitlab coming soon...')

def _get_bb_signed_commits_required(self):
raise NotImplementedError('Support for Bitbucket coming soon...')
54 changes: 54 additions & 0 deletions arboretum/auditree/evidences/repo_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- mode:python; coding:utf-8 -*-
# Copyright (c) 2020 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Repository commit evidence."""

import json

from compliance.evidence import RawEvidence


class RepoCommitEvidence(RawEvidence):
"""Repository commit raw evidence class."""

@property
def commit_signed_status(self):
"""Provide verified/signed status for each commit as a list."""
if self.content:
rs_factory = {
'gh': self._get_gh_commit_signed_status,
'gl': self._get_gl_commit_signed_status,
'bb': self._get_bb_commit_signed_status
}
if not hasattr(self, '_commit_signed_status'):
self._commit_signed_status = rs_factory[self.name[:2]]()
return self._commit_signed_status

def _get_gh_commit_signed_status(self):
commits = []
for commit in json.loads(self.content):
commits.append(
{
'sha': commit['sha'],
'url': commit['html_url'],
'signed': commit['commit']['verification']['verified']
}
)
return commits

def _get_gl_commit_signed_status(self):
raise NotImplementedError('Support for Gitlab coming soon...')

def _get_bb_commit_signed_status(self):
raise NotImplementedError('Support for Bitbucket coming soon...')
Loading

0 comments on commit 3270484

Please sign in to comment.