Skip to content

Commit afb62aa

Browse files
authored
Implement "agent" mode. (#129)
* Add fetchLocalCommands helper method. Adds a helper method to the ComplianceFetcher class. It can be used to execute commands locally and retrieve output. The output is formatted to look like it was copied from a terminal so that it can be stored as evidence. * Fix locker branch implementation. This change allows the branch to be specified via `locker.branch` in the main configuration file. A new branch is created if it doesn't exist on the remote. * Make local evidence repository path configurable. This change allows the local evidence repository path to be specified via `locker.local_path` in the main configuration file. * Implement agent mode. This change implements agent mode. Any evidence created in agent mode will be stored under the corresponding agent directory. Agents will cryptographically sign any evidence they fetch. Signed evidence can be used in checks and is automatically verified when loaded from the locker. * Add documentation explaining how to verify encrypted evidence. * Add file 'compliance/utils/fetch_local_commands' to manifest. * Attempt to import any missing fetchers from include JSON file. * Correctly set agent when loading evidence from the cache. * Add 'locker.ignore_signatures' configuration. Set 'locker.ignore_signatures' to 'true' to skip signature verification. * Use correct evidence metadata when fetching previous revisions. Ensure the 'evidence_dt' is specified when fetching evidence metadata. * Fix agent class property return types. Change following properties to correctly return a boolean type: - compliance.agent.ComplianceAgent.signable - compliance.agent.ComplianceAgent.verifiable * Add 'locker.default_branch' configuration. Defaults to 'master'. * Add 'locker.force_push' configuration. Defaults to False. * Add 'locker.agent_public_key' configuration. Allow an Agent public key to be specified in the configuration. * Update CHANGES.md for 1.22.0 release.
1 parent c8a6475 commit afb62aa

File tree

18 files changed

+1175
-68
lines changed

18 files changed

+1175
-68
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# [1.22.0](https://github.com/ComplianceAsCode/auditree-framework/releases/tag/v1.22.0)
2+
3+
- [ADDED] Agent mode for storing cryptographically signed evidence.
4+
- [ADDED] Configurable branch name for evidence repository.
5+
- [ADDED] Configurable force push to remote for evidence repository.
6+
- [ADDED] Fetcher helper for running local commands.
7+
- [FIXED] Attempt to import missing fetchers from the include JSON configuration.
8+
19
# [1.21.1](https://github.com/ComplianceAsCode/auditree-framework/releases/tag/v1.21.1)
210

311
- [FIXED] Addressed PagerDuty notifier hanging and not firing pages.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
graft compliance/templates
2+
include compliance/utils/fetch_local_commands

compliance/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- mode:python; coding:utf-8 -*-
2-
# Copyright (c) 2020 IBM Corp. All rights reserved.
2+
# Copyright (c) 2020, 2022 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -14,4 +14,4 @@
1414
# limitations under the License.
1515
"""Compliance automation package."""
1616

17-
__version__ = '1.21.1'
17+
__version__ = '1.22.0'

compliance/agent.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# -*- mode:python; coding:utf-8 -*-
2+
# Copyright (c) 2022 IBM Corp. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Compliance check automation module."""
16+
17+
import base64
18+
import hashlib
19+
from pathlib import PurePath
20+
21+
from compliance.config import get_config
22+
23+
from cryptography.exceptions import InvalidSignature
24+
from cryptography.hazmat.backends import default_backend
25+
from cryptography.hazmat.primitives import hashes, serialization
26+
from cryptography.hazmat.primitives.asymmetric import padding
27+
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
28+
29+
30+
class ComplianceAgent:
31+
"""Compliance agent class."""
32+
33+
AGENTS_DIR = 'agents'
34+
PUBLIC_KEYS_EVIDENCE_PATH = 'raw/auditree/agent_public_keys.json'
35+
36+
def __init__(self, name=None, use_agent_dir=True):
37+
"""Construct and initialize the agent object."""
38+
self._name = name
39+
self._private_key = self._public_key = None
40+
self._use_agent_dir = use_agent_dir
41+
42+
@property
43+
def name(self):
44+
"""Get agent name."""
45+
return self._name
46+
47+
@property
48+
def private_key(self):
49+
"""Get agent private key."""
50+
return self._private_key
51+
52+
@private_key.setter
53+
def private_key(self, data_bytes):
54+
"""
55+
Set agent private key.
56+
57+
:param data_bytes: The PEM encoded key data as `bytes`.
58+
"""
59+
self._private_key = serialization.load_pem_private_key(
60+
data_bytes, None, default_backend()
61+
)
62+
63+
@property
64+
def public_key(self):
65+
"""Get agent public key."""
66+
return self._public_key
67+
68+
@public_key.setter
69+
def public_key(self, data_bytes):
70+
"""
71+
Set agent public key.
72+
73+
:param data_bytes: The PEM encoded key data as `bytes`.
74+
"""
75+
if self.name:
76+
self._public_key = serialization.load_pem_public_key(data_bytes)
77+
78+
def get_path(self, path):
79+
"""
80+
Get the full evidence path.
81+
82+
:param path: The relative evidence path as a string.
83+
84+
:returns: The full evidence path as a string.
85+
"""
86+
if self.name and self._use_agent_dir:
87+
if PurePath(path).parts[0] != self.AGENTS_DIR:
88+
return str(PurePath(self.AGENTS_DIR, self.name, path))
89+
return path
90+
91+
def signable(self):
92+
"""Determine if the agent can sign evidence."""
93+
return all([self.name, self.private_key])
94+
95+
def verifiable(self):
96+
"""Determine if the agent can verify evidence."""
97+
return all([self.name, self.public_key])
98+
99+
def load_public_key_from_locker(self, locker):
100+
"""
101+
Load agent public key from locker.
102+
103+
:param locker: A locker of type :class:`compliance.locker.Locker`.
104+
"""
105+
if not self.name:
106+
return
107+
try:
108+
public_keys = locker.get_evidence(self.PUBLIC_KEYS_EVIDENCE_PATH)
109+
public_key_str = public_keys.content_as_json[self.name]
110+
self.public_key = public_key_str.encode()
111+
except Exception:
112+
self._public_key = None # Missing public key evidence.
113+
114+
def hash_and_sign(self, data_bytes):
115+
"""
116+
Hash and sign evidence using the agent private key.
117+
118+
:param data_bytes: The data to sign as `bytes`.
119+
120+
:returns: A `tuple` containing the hexadecimal digest string and the
121+
base64 encoded signature string. Returns tuple `(None, None)` if the
122+
agent is not configured to sign evidence.
123+
"""
124+
if not self.signable():
125+
return None, None
126+
hashed = hashlib.sha256(data_bytes)
127+
signature = self.private_key.sign(
128+
hashed.digest(),
129+
padding.PSS(
130+
mgf=padding.MGF1(hashes.SHA256()),
131+
salt_length=padding.PSS.MAX_LENGTH
132+
),
133+
Prehashed(hashes.SHA256())
134+
)
135+
return hashed.hexdigest(), base64.b64encode(signature).decode()
136+
137+
def verify(self, data_bytes, signature_b64):
138+
"""
139+
Verify evidence using the agent public key.
140+
141+
:param data_bytes: The data to verify as `bytes`.
142+
:param signature_b64: The base64 encoded signature string.
143+
144+
:returns: `True` if data can be verified, else `False`.
145+
"""
146+
if not self.verifiable():
147+
return False
148+
try:
149+
self.public_key.verify(
150+
base64.b64decode(signature_b64),
151+
data_bytes,
152+
padding.PSS(
153+
mgf=padding.MGF1(hashes.SHA256()),
154+
salt_length=padding.PSS.MAX_LENGTH
155+
),
156+
hashes.SHA256()
157+
)
158+
return True
159+
except InvalidSignature:
160+
return False
161+
162+
@classmethod
163+
def from_config(cls):
164+
"""Load agent from configuration."""
165+
config = get_config()
166+
agent = cls(
167+
name=config.get('agent_name'),
168+
use_agent_dir=config.get('use_agent_dir', True)
169+
)
170+
private_key_path = config.get('agent_private_key')
171+
public_key_path = config.get('agent_public_key')
172+
if private_key_path:
173+
with open(private_key_path, 'rb') as key_file:
174+
agent.private_key = key_file.read()
175+
agent.public_key = agent.private_key.public_key().public_bytes(
176+
encoding=serialization.Encoding.PEM,
177+
format=serialization.PublicFormat.SubjectPublicKeyInfo
178+
)
179+
elif public_key_path:
180+
with open(public_key_path, 'rb') as key_file:
181+
agent.public_key = key_file.read()
182+
return agent

0 commit comments

Comments
 (0)