Skip to content

Commit

Permalink
Added tests, renamed file
Browse files Browse the repository at this point in the history
  • Loading branch information
jc0b committed May 10, 2024
1 parent 06aac01 commit 9ec9c0c
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 45 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Run Tests
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Generate Nudge file
run: |
python3 -m pip install -r requirements.txt
./nudge-auto-updater.py -s file://${GITHUB_WORKSPACE}/tests/test-latest/feed.json -c ${GITHUB_WORKSPACE}/tests/test-latest/configuration.yml -n ${GITHUB_WORKSPACE}/tests/test-latest/nudge-config.json
- name: Run tests
run: |
[[ $(jq -r '.OSVersions.[0].SecurityReleases.[0].ProductVersion' ${GITHUB_WORKSPACE}/tests/test-latest/feed.json) == $(jq -r '.osVersionRequirements.[0].requiredMinimumOSVersion' ${GITHUB_WORKSPACE}/tests/test-latest/nudge-config.json) ]]
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# nudge-auto-updater
A tool to demo how you can update [Nudge](https://github.com/macadmins/Nudge) JSON configuration definitions automatically.
`nudge-auto-updater` is a tool that leverages [SOFA](https://sofa.macadmins.io) in combination with [VulnCheck](https://docs.vulncheck.com/) to detect new macOS updates, triage the severity of the CVEs fixed, and update your JSON [Nudge](https://github.com/macadmins/Nudge) configuration appropriately.

Leverages [SOFA](https://sofa.macadmins.io) for the macOS update feed, and [NIST's National Vulnerability Database REST API](https://nvd.nist.gov/developers/vulnerabilities) for grabbing info about CVEs.
A VulnCheck API key is currently required to use this script - without it, CVE lookups can't be performed.

## Configuration
You can configure this program by putting a `configuration.yml` file in the same directory as the script.
Under the ket `targets`, this `configuration.yml` file should contain a list of `osVersionRequirements`, the keys of which are documented below:
Under the key `targets`, this `configuration.yml` file should contain a list of `osVersionRequirements`, the keys of which are documented below:
| Key | Type | Description |
|-----------------------|--------|----------------------|
| `target` | string | Specifies the `targetedOSVersionsRule` in Nudge. |
| `update_to` | string | Specifies the macOS version this target should update to. This value can be "latest" if the `requiredMinimumOSVersion` should be the latest version of macOS. Otherwise this value can be a major version (e.g. 13), a minor version (e.g. 13.1) or a specific patch version (e.g. 13.1.1). In this case the `requiredMinimumOSVersion` will be set to the newest macOS version with a major version, minor version or patch version less than or equal to the specified value. |

To do: describe rest of keys

If the `configuration.yml` file is missing this program will only update the Nudge configuration `osVersionRequirements` for the default `targetedOSVersionsRule` to the latest version of macOS.
If the `configuration.yml` file is missing this script will only update the Nudge configuration `osVersionRequirements` for the default `targetedOSVersionsRule` to the latest version of macOS.
The specified configuration will only update existing `osVersionRequirements` - it will not create new ones.
An example file is included in this project.
117 changes: 81 additions & 36 deletions main.py → nudge-auto-updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import datetime
import json
import logging
import optparse
import os
import re
import sys
import urllib.error
import urllib.request

CONFIG_FILE_NAME = "configuration.yml"
DEFAULT_CONFIG_FILE_NAME = "configuration.yml"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
HEADERS = {'accept': 'application/json', 'User-Agent': 'nudge-auto-updater/1.0'}
DEFAULT_CONFIG = {
Expand All @@ -17,18 +18,9 @@
"default_deadline_days" : 14,
"urgent_deadline_days" : 7
}

using_default_config = False

try:
import yaml
except ModuleNotFoundError as e:
if os.path.exists(CONFIG_FILE_NAME):
logging.error(f"Can't read configuration file: {e}")
sys.exit(1)
else:
using_default_config = True
logging.warning("PyYAML library could not be loaded, but no configuration file is present.\nWill continue with default settings.")
DEFAULT_NUDGE_FILENAME = "nudge-config.json"
DEFAULT_SOFA_FEED = "https://sofa.macadmins.io/v1/macos_data_feed.json"
VERSION="0.0.1"

# ----------------------------------------
# Version
Expand Down Expand Up @@ -113,14 +105,14 @@ def __lt__(self, other):
def get_nudge_config() -> dict:
logging.info("Loading Nudge config...")
try:
f = open("nudge-config.json")
f = open(nudge_filename)
try:
data = json.load(f)
except Exception as e:
logging.error("Unable to load nudge-config.json")
logging.error(f"Unable to load {nudge_filename}")
sys.exit(1)
except Exception as e:
logging.error("Unable to open nudge-config.json")
logging.error(f"Unable to open {nudge_filename}")
sys.exit(1)

logging.info("Successfully loaded Nudge config!")
Expand All @@ -137,10 +129,10 @@ def read_nudge_requirements(d:dict):

def write_nudge_config(d:dict):
try:
with open('nudge-config.json', 'w') as f:
with open(nudge_filename, 'w') as f:
json.dump(d, f, indent=4)
except Exception as e:
logging.error("Unable to write to nudge-config.json")
logging.error(f"Unable to write to {nudge_filename}")
sys.exit(1)

def update_nudge_file_dict(d:dict, target, version, url, days):
Expand All @@ -155,15 +147,13 @@ def update_nudge_file_dict(d:dict, target, version, url, days):
d["osVersionRequirements"][i]["requiredInstallationDate"] = datestr
d["osVersionRequirements"][i]["requiredMinimumOSVersion"] = str(version)
return d
logging.error(f"Unable to find target {target} in nudge-config.json.")
logging.error(f"Unable to find target {target} in {nudge_filename}.")
sys.exit(1)

def adjust_url(url, change):
print(type(url))
print(url)
i = url.rfind("/") + 1
url = url[:i]
url += "/" + change
url += change
return url

def adjust_date_str(datestr, days):
Expand All @@ -177,7 +167,7 @@ def adjust_date_str(datestr, days):
# macOS
# ----------------------------------------
def get_macos_data():
req = urllib.request.Request(url="https://sofa.macadmins.io/v1/macos_data_feed.json", headers=HEADERS, method="GET")
req = urllib.request.Request(url=sofa_url, headers=HEADERS, method="GET")
try:
response = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
Expand Down Expand Up @@ -218,6 +208,8 @@ def process_url(s:str):
return parts[-1]

def get_CVE_scores(s:str, b:bool):
vulncheck_headers = HEADERS
vulncheck_headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(url=f"https://api.vulncheck.com/v3/index/nist-nvd2?cve={s}", headers=HEADERS, method="GET")
try:
response = urllib.request.urlopen(req)
Expand Down Expand Up @@ -247,21 +239,21 @@ def read_CVE_scores(d:dict, b:bool):
# ----------------------------------------
def get_config() -> dict:
global using_default_config
if not os.path.exists(CONFIG_FILE_NAME):
if not os.path.exists(config_file):
using_default_config = True
logging.warning("No configuration file is present. Will continue with default settings.")
if using_default_config:
return DEFAULT_CONFIG
with open(CONFIG_FILE_NAME, "r") as config_yaml:
logging.info(f"Loading {CONFIG_FILE_NAME} ...")
with open(config_file, "r") as config_yaml:
logging.info(f"Loading {config_file} ...")
try:
result = yaml.safe_load(config_yaml)
logging.info(f"Successfully loaded {CONFIG_FILE_NAME}!")
logging.info(f"Successfully loaded {config_file}!")
if result == None or len(result) < 1:
return DEFAULT_CONFIG
return result
except yaml.YAMLError as e:
logging.error(f"Unable to load {CONFIG_FILE_NAME}")
logging.error(f"Unable to load {config_file}")
sys.exit(1)
return result

Expand Down Expand Up @@ -433,7 +425,7 @@ def main():
if nudge_requirements[target["target"]]["version"] < latest_macos_releases[0]:
is_uptodate = False
new_macos_release = latest_macos_releases[0]
logging.info(f"Nudge configuration for target {target['target']} needs to be updated from {nudge_requirements[target['target']]['version']} to {new_macos_release})")
logging.info(f"Nudge configuration for target \"{target['target']}\" needs to be updated from {nudge_requirements[target['target']]['version']} to {new_macos_release})")
else:
is_uptodate = True
else:
Expand All @@ -446,7 +438,7 @@ def main():
new_macos_release = macos_release
break
if is_uptodate:
logging.info(f"Nudge configuration for target {target['target']} is already up to date.")
logging.info(f"Nudge configuration for target \"{target['target']}\" is already up to date.")
else:
# nudge is not up to date! Is the new update urgent?
# get security metrics
Expand All @@ -468,6 +460,10 @@ def main():
write_nudge_config(nudge_file_dict)
logging.info("Nudge configuration updated.")

def config_help(msg):

sys.exit(1)

def setup_logging():
logger = logging.getLogger(__name__)
logging.basicConfig(
Expand All @@ -478,12 +474,61 @@ def setup_logging():

if __name__ == '__main__':
setup_logging()
try:
global api_key
api_key = os.environ["VULNCHECK_API_KEY"]
HEADERS["Authorization"] = f"Bearer {api_key}"
usage = """usage: %prog [options]\nScript to update a Nudge JSON configuration file."""
parser = optparse.OptionParser(usage=usage, version=VERSION)
parser.add_option('--sofa-url', '-s', dest='sofa_url',
help="Custom SOFA feed URL. Should include the path to macos_data_feed.json.\nDefaults to https://sofa.macadmins.io/v1/macos_data_feed.json")
parser.add_option('--nudge-file', '-n', dest='nudge_file',
help="The Nudge JSON config file to update.\nDefaults to nudge-config.json")
parser.add_option('--api-key', '-a', dest='api_key',
help="A VulnCheck API key for getting CVE data. It is required to either set this argument, or the VULNCHECK_API_KEY environment variable.")
parser.add_option('--config-file', '-c', dest='config_file',
help="The path to a yaml-formatted file containing the configuration for nudge-auto-updater")

options, arguments = parser.parse_args()
global sofa_url
global nudge_filename
global api_key
global config_file

if options.sofa_url:
sofa_url = options.sofa_url
logging.info(f"Using {sofa_url} as a custom SOFA feed...")
else:
sofa_url = DEFAULT_SOFA_FEED

if not options.nudge_file:
nudge_filename = DEFAULT_NUDGE_FILENAME
else:
nudge_filename = options.nudge_file

if os.environ.get("VULNCHECK_API_KEY"):
api_key = os.environ.get("VULNCHECK_API_KEY")
logging.info("Using the provided VulnCheck API key...")
elif options.api_key:
api_key = options.api_key
logging.info("Using the provided VulnCheck API key...")
except KeyError as e:
logging.error(f"The {e} environment variable is not set. A VulnCheck API key is required to use this script.\nSee https://docs.vulncheck.com/getting-started/api-tokens for more.")
else:
logging.error(f"A VulnCheck API key is required to use this script. Please set it using either the VULNCHECK_API_KEY environment variable, or the --api-key argument.\n\tSee https://docs.vulncheck.com/getting-started/api-tokens for more.")
sys.exit(1)

if options.config_file:
config_file = options.config_file
logging.info(f"Using {config_file} for deferral configuration...")
else:
config_file = DEFAULT_CONFIG_FILE_NAME

global using_default_config
using_default_config = False

try:
import yaml
except ModuleNotFoundError as e:
if os.path.exists(config_file):
logging.error(f"Can't read configuration file: {e}")
sys.exit(1)
else:
using_default_config = True
logging.warning("PyYAML library could not be loaded, but no configuration file is present.\nWill continue with default settings.")

main()
10 changes: 5 additions & 5 deletions nudge-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@
},
"osVersionRequirements": [
{
"aboutUpdateURL_disabled": "https://support.apple.com/en-us//HT214096",
"aboutUpdateURL_disabled": "https://support.apple.com/en-us/HT214096",
"aboutUpdateURLs": [
{
"_language": "en",
"aboutUpdateURL": "https://support.apple.com/en-us//HT214096"
"aboutUpdateURL": "https://support.apple.com/en-us/HT214096"
}
],
"requiredInstallationDate": "2024-04-22T14:00:00Z",
"requiredInstallationDate": "2024-07-15T14:00:00Z",
"requiredMinimumOSVersion": "14.4.1",
"targetedOSVersionsRule": "default"
},
{
"aboutUpdateURL_disabled": "https://support.apple.com/en-us//HT214095",
"aboutUpdateURL_disabled": "https://support.apple.com/en-us/HT214095",
"aboutUpdateURLs": [
{
"_language": "en",
"aboutUpdateURL": "https://support.apple.com/en-us//HT214095"
"aboutUpdateURL": "https://support.apple.com/en-us/HT214095"
}
],
"requiredInstallationDate": "2024-04-23T14:00:00Z",
Expand Down
30 changes: 30 additions & 0 deletions tests/test-latest/configuration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
targets:
- target : "default"
update_to : "latest"
cve_urgency_conditions:
max_baseScore : 10
average_baseScore : 8
max_exploitabilityScore : 10
average_exploitabilityScore : 8
max_impactScore : 10
average_impactScore : 8
number_CVEs : 10
number_actively_exploited_CVEs : 5
fraction_actively_exploited_CVEs : 0.7
formulas:
- comparison : "average"
formula : "baseScore * exploitabilityScore * impactScore"
threshhold : 500
- comparison : "max"
formula : "baseScore * exploitabilityScore * impactScore * is_actively_exploited"
threshhold : 200
- comparison : "sum"
formula : "baseScore * impactScore * is_actively_exploited"
threshhold : 300
- comparison : "n_above"
formula : "baseScore * impactScore * is_actively_exploited"
n : 2
threshhold : 300
default_deadline_days : 14
urgent_deadline_days : 7
Loading

0 comments on commit 9ec9c0c

Please sign in to comment.