Skip to content

Commit

Permalink
Merge pull request #10 from GSA-TTS/add-user-report
Browse files Browse the repository at this point in the history
Add cloud.gov user report
  • Loading branch information
rahearn authored Oct 30, 2024
2 parents 47f3426 + 1a42311 commit 2972f3d
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11.9
3.11.10
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

ARG PYTHON_VERSION=3.11.9
ARG PYTHON_VERSION=3.11.10
FROM python:${PYTHON_VERSION}-slim AS base

# Prevents Python from writing pyc files.
Expand Down
67 changes: 67 additions & 0 deletions auditree/checks/test_cf_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,70 @@ def test_space_ssh_disabled(self, space):

def get_reports(self):
return ["cf/space-ssh.md"]

class UserRoleReport(ComplianceCheck):
# failures are covered by empty evidence / abandoned evidence, so this is just gathering the raw evidence into a format
# useful for a human-readable report

@property
def title(self):
return "User Role Report"

@property
def users(self):
return self._user_map()

@classmethod
def _user_map(cls):
if not hasattr(cls, '_users'):
cls._users = {}
return cls._users

@classmethod
def _guid_exists(cls, guid):
return guid in cls._user_map()


@classmethod
def _add_user(cls, guid, user):
cls._user_map()[guid] = {"user_name": user["user_name"], "roles": user["roles"]}


@classmethod
def _append_user_roles(cls, guid, user):
cls._user_map()[guid]["roles"] += user["roles"]


@with_raw_evidences("raw/cf/org-roles.json")
def test_org_roles_report(self, evidence):
self._process_users(evidence)


@parameterized.expand(get_config().get("gov.cloud.space-names"), skip_on_empty=True)
def test_user_role_report(self, space):
evidence_path = f"raw/cf/space-{space}-user-roles.json"
with evidences(self, evidence_path) as evidence:
self._process_users(evidence)


def _process_users(self, evidence):
contents = json.loads(evidence.content)
for bot in (u for u in contents if u["user_type"] == "Bot"):
guid = bot["user_guid"]
if self._guid_exists(guid):
self._append_user_roles(guid, bot)
else:
self._add_user(guid, bot)
self.add_successes("Service Accounts", guid)

for user in (u for u in contents if u["user_type"] != "Bot"):
guid = user["user_guid"]
if self._guid_exists(guid):
self._append_user_roles(guid, user)
else:
self._add_user(guid, user)
self.add_successes("Users", guid)


def get_reports(self):
return ["cf/user-roles.md"]
3 changes: 2 additions & 1 deletion auditree/controls.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"arboretum.auditree.checks.test_abandoned_evidence.AbandonedEvidenceCheck": ["devtools.arboretum.accred"],
"arboretum.auditree.checks.test_compliance_config.ComplianceConfigCheck": ["devtools.arboretum.accred"],
"arboretum.auditree.checks.test_empty_evidence.EmptyEvidenceCheck": ["devtools.arboretum.accred"],
"checks.test_cf_ssh.SpaceSSHDisabledCheck": ["devtools.cloudgov.accred"]
"checks.test_cf_ssh.SpaceSSHDisabledCheck": ["devtools.cloudgov.accred"],
"checks.test_cf_ssh.UserRoleReport": ["devtools.cloudgov.accred"]
}
25 changes: 25 additions & 0 deletions auditree/fetchers/fetch_cf_metadata.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import json
import subprocess

from cloudfoundry_client.client import CloudFoundryClient

from compliance.evidence import raw_evidence
from compliance.fetch import ComplianceFetcher

from compliance.config import get_config

from parameterized import parameterized

from utils.cf_roles import Role, RoleCollector, retrieve_cf_client

class FetchCfMetadata(ComplianceFetcher):
def fetch_org_roles(self):
with raw_evidence(self.locker, "cf/org-roles.json") as evidence:
if evidence:
client, org_guid = retrieve_cf_client()
role_collector = RoleCollector()
for role in map(Role, client.v3.roles.list(organization_guids=org_guid, include="user")):
role_collector.add(role)
evidence.set_content(role_collector.to_json())


@parameterized.expand(get_config().get("gov.cloud.space-names"), skip_on_empty=True)
def fetch_space_roles(self, space):
evidence_path = f"cf/space-{space}-user-roles.json"
with raw_evidence(self.locker, evidence_path) as evidence:
if evidence:
client, space_guid = retrieve_cf_client(space)
role_collector = RoleCollector()
for role in map(Role, client.v3.roles.list(space_guids=space_guid, include="user")):
role_collector.add(role)
evidence.set_content(role_collector.to_json())

@parameterized.expand(get_config().get("gov.cloud.space-names"), skip_on_empty=True)
def fetch_prod_ssh(self, space):
evidence_path = f"cf/space-{space}-ssh.json"
Expand Down
14 changes: 14 additions & 0 deletions auditree/templates/reports/cf/user-roles.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{#- -*- mode:jinja2; coding: utf-8 -*- -#}

# {{ test.title }} {{ now.strftime('%Y-%m-%d') }}

The following users have access to our cloud.gov infrastructure.

{% for section_heading, user_guids in all_successes.items() -%}
## {{ section_heading }}
{% for guid in user_guids: %}
{% set user = test.users[guid] -%}
* **User**: {{ user["user_name"] }}<br />
**Roles**: {{ user["roles"] | join(', ') }}
{% endfor %}
{% endfor %}
81 changes: 81 additions & 0 deletions auditree/utils/cf_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from subprocess import check_output
import json
from compliance.config import get_config
from cloudfoundry_client.client import CloudFoundryClient

client = CloudFoundryClient.build_from_cf_config()

def retrieve_cf_client(space_name=None):
if space_name is None:
guid = check_output(f"cf org {get_config().get('gov.cloud.org-name')} --guid", shell=True).decode().strip()
else:
guid = check_output(f"cf space {space_name} --guid", shell=True).decode().strip()
return (client, guid)


class RoleCollector:
def __init__(self):
self._map = {}

def add(self, role):
user = role.user
if self._map.get(user.guid) is None:
self._map[user.guid] = {"user": user, "roles": [role]}
else:
self._map[user.guid]["roles"].append(role)

def to_json(self):
data = []
for user_roles in self._map.values():
user = user_roles["user"]
roles = user_roles["roles"]
data.append({
"user_guid": user.guid,
"user_type": user.type,
"user_name": user.username,
"roles": list((role.description for role in roles))
})
return json.dumps(data)


class User:
def __init__(self, entity):
self.guid = entity["guid"]
self._username = entity["username"]
self._is_service_account = entity["origin"] != "gsa.gov"
self.type = "Bot" if self._is_service_account else "User"

@property
def username(self):
if self._is_service_account:
return client.v3.service_credential_bindings.get(
self._username, include="service_instance"
).service_instance()["name"]
else:
return self._username


class Space:
def __init__(self, entity):
self.name = entity["name"]


class Role:
def __init__(self, entity):
self._fields = entity
self.type = entity["type"]
self.user = User(entity.user())

@property
def description(self):
if self.space:
return f"{self.type} in space \"{self.space.name}\""
else:
return self.type

@property
def space(self):
try:
return Space(self._fields.space())
except AttributeError:
return None
1 change: 0 additions & 1 deletion bin/fetch
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ set -e

python c2p/compliance_to_policy.py -c cdef.json -o auditree/auditree.json

cf api api.fr.cloud.gov
cf auth

cd auditree
Expand Down
5 changes: 4 additions & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ if [ "$1" != "init" ]; then
# 1) configure git
git config --global user.name "$GIT_EMAIL"
git config --global user.email "$GIT_EMAIL"

# 2) set cloud.gov api endpoint
cf api api.fr.cloud.gov > /dev/null
fi

# 2) call program
# 3) call program
exec "$@"
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
auditree-framework @ git+https://github.com/ComplianceAsCode/auditree-framework.git
auditree-arboretum ~= 0.17
auditree-prune @ git+https://github.com/ComplianceAsCode/auditree-prune.git
auditree-plant @ git+https://github.com/rahearn/auditree-plant.git
auditree-plant @ git+https://github.com/ComplianceAsCode/auditree-plant.git
compliance-to-policy ~= 0.4
cloudfoundry-client ~= 1.37

0 comments on commit 2972f3d

Please sign in to comment.