Skip to content

Commit

Permalink
Merge pull request #1 from novateams/AWK-NOVA
Browse files Browse the repository at this point in the history
version: 0.0.2
  • Loading branch information
tavipoldma authored Sep 15, 2023
2 parents 8225284 + 20d2d1c commit e1de27c
Show file tree
Hide file tree
Showing 27 changed files with 1,285 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Contributing to the Project

By making contributions to this project, you acknowledge that these contributions are either your original work or have been authorized by your employer. Additionally, you grant an unrestricted, perpetual, and irrevocable copyright license to all present and future users and developers of the project. This license is granted in accordance with the existing license of the project.

Please note that by submitting your contributions, you affirm that you have the necessary rights to grant this license and that your contributions will be made available under the terms and conditions of the project's existing license.

Thank you for your contributions to the project!
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Updating collection version, git tag and release

on:
push:
branches:
- main

jobs:
version_collection_and_tag:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Configuring collection & tag versions
run: |
target_file="nova/core/galaxy.yml"
# Configuring git
git config --global user.name "Nova CI"
git config --global user.email "[email protected]"
# Updating the version in the galaxy.yml file
version_row_old=$(grep "version: " $target_file)
version=$(echo $version_row_old | cut -d: -f2)
major=$(echo $version | cut -d. -f1)
minor=$(echo $version | cut -d. -f2)
patch=$(echo $version | cut -d. -f3)
patch_new=$(( $patch+1 ))
version_row_new="version: $major.$minor.$patch_new"
sed -i "s/$version_row_old/$version_row_new/" $target_file
TAG_NAME="v$major.$minor.$patch_new"
echo "LATEST_TAG=$TAG_NAME" >> $GITHUB_ENV
# Adding the changed file to git
git add $target_file
# Committing the change
git commit -m "Set nova.core collection version to $major.$minor.$patch_new"
git push
# Tagging and pushing the change
git tag $TAG_NAME
git push origin $TAG_NAME
# Creating temp changelog file
git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^^)..HEAD > CHANGELOG.md
- uses: ncipollo/release-action@v1
with:
tag: ${{ env.LATEST_TAG }}
bodyFile: CHANGELOG.md
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.githooks
.vscode
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# nova.core
Ansible collection for roles and plugins
# Ansible Collection - nova.core

This is an Ansible collection consisting of some roles and a an inventory plugin for [Providentia](https://github.com/ClarifiedSecurity/Providentia) maintained by the Nova team. This collection is a culmination of years for cyber defense exercises and is maintained by:

- [Clarified Security](https://www.clarifiedsecurity.com)
- [CCDCOE](https://ccdcoe.org/)
56 changes: 56 additions & 0 deletions nova/core/galaxy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
### REQUIRED
# The namespace of the collection. This can be a company/brand/organization or product namespace under which all
# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with
# underscores or numbers and cannot contain consecutive underscores
namespace: nova

# The name of the collection. Has the same character restrictions as 'namespace'
name: core

# The version of the collection. Must be compatible with semantic versioning
version: 0.0.1

# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
readme: README.md

# A list of the collection's content authors. Can be just the name or in the format 'Full Name <email> (url)
# @nicks:irc/im.site#channel'
authors:
- https://github.com/novateams

### OPTIONAL but strongly recommended
# A short summary description of the collection
description: This is a collection of public roles nad plugins that are developed by the Nova team. These roles go very well with Catapult https://github.com/ClarifiedSecurity/catapult but can be used separately.

# The path to the license file for the collection. This path is relative to the root of the collection. This key is
# mutually exclusive with 'license'
license:
- AGPL-3.0-or-later

# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
# requirements as 'namespace' and 'name'
tags: []

# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
# collection label 'namespace.name'. The value is a version range
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
# range specifiers can be set and are separated by ','
dependencies: {}

# The URL of the originating SCM repository
repository: https://github.com/novateams/nova.core

# The URL to any online docs
documentation: COMING SOON

# The URL to the homepage of the collection/project
homepage: https://github.com/novateams/nova.core

# The URL to the collection issue tracker
issues: https://github.com/novateams/nova.core/issues

# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry',
# and '.git' are always filtered
build_ignore: []
2 changes: 2 additions & 0 deletions nova/core/meta/runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
requires_ansible: ">=2.13.12"
209 changes: 209 additions & 0 deletions nova/core/plugins/inventory/providentia_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
DOCUMENTATION = """
name: providentia_v3
plugin_type: inventory
short_description: Providentia inventory source
requirements:
- requests >= 2.18.4
- requests_oauthlib
- oauthlib
description:
- Get inventory hosts and groups from Providentia.
- Uses a YAML configuration file that ends with providentia.(yml|yaml).
options:
plugin:
description: token that ensures this is a source file for the 'providentia' plugin.
required: True
providentia_host:
description: Root URL to Providentia.
type: string
required: True
exercise:
description: Exercise abbreviation which defines configuration to populate inventory with.
type: string
required: True
sso_token_url:
description: The endpoint where token may be obtained for Providentia
sso_client_id:
description: SSO client id for Providentia.
type: string
default: "Providentia"
credentials_lookup_env:
description: ENV var used to lookup Providentia credentials KeePass path
type: string
default: KEEPASS_DEPLOYER_CREDENTIALS_PATH
required: False
"""

from typing import DefaultDict
import requests
import os
import json
import socket
import aiohttp
import asyncio
from oauthlib.oauth2 import LegacyApplicationClient
from pykeepass import PyKeePass
from requests_oauthlib import OAuth2Session
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.utils.vars import combine_vars, load_extra_vars
from pprint import pprint

class InventoryModule(BaseInventoryPlugin):
NAME = 'providentia_v3'

def verify_file(self, path):
if super(InventoryModule, self).verify_file(path):
return True
return False

def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self._read_config_data(path)
# merge extra vars
self._options = combine_vars(self._options, load_extra_vars(loader))

asyncio.run(self.run())

async def run(self):
self.init_inventory()
await self.store_access_token()

async with aiohttp.ClientSession() as session:
self._session = session
await self.fetch_environment()
await self.fetch_groups()
await self.fetch_hosts()

def init_inventory(self):
self.inventory.add_group("all")

self.inventory.set_variable("all", "providentia_api_version", 3)

async def store_access_token(self):
keepass_creds = os.environ.get(self.get_option('credentials_lookup_env'),"").strip()
sso_creds = self.fetch_keepass_creds(keepass_creds)

self._access_token = self.fetch_access_token(sso_creds)

def fetch_keepass_creds(self, creds_path):
kp_soc = "/tmp/ansible-keepass.sock"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(kp_soc)

username = {'attr': "username", 'path': creds_path}
sock.send(json.dumps(username).encode())
username = json.loads(sock.recv(1024).decode())

password = {'attr': "password", 'path': creds_path}
sock.send(json.dumps(password).encode())
password = json.loads(sock.recv(1024).decode())

sock.close()

if(username['status']=='error' or password['status']=='error'):
raise Exception('Error retrieving credentials from Keepass')

return {
'username': username['text'],
'password': password['text']
}

async def fetch_environment(self):
event = await self.fetch_from_providentia('')
for key,value in event['result'].items():
self.inventory.set_variable("all", key, value)

async def fetch_groups(self):
groups = await self.fetch_from_providentia('tags')

# Add groups to inventory
for group_data in groups['result']:
group = group_data['id']
group_vars = group_data['config_map']
priority = group_data.get('priority')

self.inventory.add_group(group)

# Add group specific variables to group
for key, value in group_vars.items():
self.inventory.set_variable(group, key, value)

if priority:
self.inventory.set_variable(group, 'ansible_group_priority', int(priority))

# Add groups to inventory
# We do this in separate loop because of groups can reference to
# child groups that may not have been added to inventory already
for group_data in groups['result']:
group = group_data['id']
group_children = group_data['children']

for child_group in group_children:
self.inventory.add_child(group, child_group)

async def fetch_hosts(self):
hosts = await self.fetch_from_providentia('inventory')

# List of keys that should be excluded from host variables to avoid endless recursion and overwriting
excluded_keys = ["id", "instances"]

# Creating a new dictionary with filtered parent vars using first host as a template since all of the host have the same keys
filtered_parent_vars = {key: value for key, value in hosts['result'][0].items() if key not in excluded_keys}

# Add hosts to inventory
for host in hosts['result']:
for host_instance in host.get('instances', []):
host_instance_id = host_instance['id']
self.inventory.add_host(host_instance_id)

self.inventory.set_variable(host_instance_id, "main_id", host['id'])

for var_name in filtered_parent_vars:
if var_name in host:
self.inventory.set_variable(host_instance_id, var_name, host[var_name])

for key, value in host_instance.items():
self.inventory.set_variable(host_instance_id, key, value)

for group in host.get('tags', []):
self.inventory.add_child(group, host_instance_id)

for group in host_instance.get('tags', []):
self.inventory.add_child(group, host_instance_id)

async def fetch_from_providentia(self, endpoint=""):
providentia_host = self.get_option('providentia_host')
exercise = self.get_option('exercise')

url = f"{providentia_host}/api/v3/{exercise}/{endpoint}"

headers = {
'Authorization': f"{self._access_token['token_type']} {self._access_token['access_token']}"
}
async with self._session.get(url, headers=headers) as response:
if response.status == 200:
return await response.json()

if response.status == 401:
raise Exception('Providentia responded with 401: Unauthenticated')

if response.status == 403:
raise Exception('Requested token is not authorized to perform this action')

if response.status == 404:
raise Exception('Providentia responded with 404: not found')

if response.status == 500:
raise Exception('Providentia responded with 500: server error')

def fetch_access_token(self, creds):
client_id = self.get_option('sso_client_id')
oauth = OAuth2Session(client=LegacyApplicationClient(client_id=client_id))
token = oauth.fetch_token(
token_url=self.get_option('sso_token_url'),
username=creds['username'],
password=creds['password'],
client_id=client_id)

return token
27 changes: 27 additions & 0 deletions nova/core/roles/create/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Role Name

This role is used to create Virtual Machines in different environments. Currently supported environments are:

- AWS
- Linode
- VMware vSphere
- VMWare Workstation

## Requirements

none

## Role Variables

Refer to the [defaults/main.yml](https://github.com/novateams/nova.core/blob/main/nova/core/roles/create/defaults/main.yml) file for a list of variables and their default values.

## Dependencies

Depending on the environment you want to create the VM in, you will need to install the following Ansible collections:

- amazon.aws
- community.aws
- vmware.vmware_rest
- community.vmware

## Example
Loading

0 comments on commit e1de27c

Please sign in to comment.