Skip to content

Commit

Permalink
Merge pull request #11 from FiniteStateInc/v0.1.1
Browse files Browse the repository at this point in the history
v0.1.1
  • Loading branch information
nickvido authored Dec 12, 2023
2 parents b65b414 + 67637d2 commit c83a081
Show file tree
Hide file tree
Showing 15 changed files with 5,565 additions and 4,136 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The cloud-based SaaS platform for SBOM and Product Security management enables y
* Generate and manage SBOMs in any format to create software transparency
* Orchestrate and correlate scan findings from over 120 top scanning tools
* Monitor AppSec and Product Security risk across product portfolios to visualize risk scoring and prioritize critical findings
* Leverage world-class binary SCA to generate the most thorough and accurate SBOMs available with world-class binary SCA
* Leverage world-class binary SCA to generate the most thorough and accurate SBOMs available

# [Finite State](https://finitestate.io) SDK

Expand Down
7,104 changes: 3,837 additions & 3,267 deletions docs/finite_state_sdk.html

Large diffs are not rendered by default.

1,220 changes: 675 additions & 545 deletions docs/finite_state_sdk/queries.html

Large diffs are not rendered by default.

307 changes: 171 additions & 136 deletions docs/finite_state_sdk/token_cache.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/search.js

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# Finite State Python SDK Examples

In this directory you can find examples for doing common tasks using the SDK.


# compare_versions.py

Compares two asset versions to show Findings that were introduced or remediated, and which software components were added, updated, or removed.

# create_a_new_product.py

How to create a new Product programatically.

# custom_query.py

If you want to run custom GraphQL queries using all the available fields in the API.

# download_reports.py

Shows how to download PDF and CSV reports for an asset version.

# download_sboms.py

For programmatically downloading SBOMs for an asset version.

# generate_products_csv.py

Creates a CSV report of high level information about products.

# get_findings.py

Getting all the Findings for an asset version, with filters by type, such as "CVE".

# get_product_and_asset_information.py

Querying for all the product and asset version using the SDK.

# get sbom_for_an_asset_version.py

Gets the entire list of software components for an asset version, with filters by type, such as "OPERATING SYSTEM".

# paginated_query.py

How to make custom queries using pagination and helper methods in the SDK.

# search_sbom.py

How to use the `search_sbom` function of the SDK to search for components by name and version, and specify whether the search should be case-sensitive or not.

# upload_test_results.py

How to programmatically upload test results (e.g. SBOMs or Third Party Scanners). Basically a one-liner you can add to your CI systems.

# uploading_a_binary.py

How to programmatically upload a binary image (e.g. a firmware or system image). Basically a one-liner you can add to your CI systems.
132 changes: 107 additions & 25 deletions examples/compare_versions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import argparse
import json
import os
import semver
from dotenv import load_dotenv

import sys

import finite_state_sdk
from finite_state_sdk.token_cache import TokenCache

Expand All @@ -26,6 +29,47 @@
"""


def compare_cves(version1_findings, version2_findings):
cve_changes = []

version1_cve_ids = []
version2_cve_ids = [finding['cves'][0]['cveId'] for finding in version2_findings]

version2_skip_list = []

# double check for findings that were resolved
for finding in version1_findings:
cve_id = finding['cves'][0]['cveId']
skip = False
if finding['currentStatus'] is not None:
if finding['currentStatus']['status'] == 'NOT_AFFECTED' or finding['currentStatus']['status'] == 'FIXED':
skip = True
version2_skip_list.append(cve_id)

if not skip:
version1_cve_ids.append(cve_id)

# Findings that were introduced
for finding in version2_findings:
cve_id = finding['cves'][0]['cveId']
if cve_id in version2_skip_list:
continue

if cve_id not in version1_cve_ids:
affects = finding['affects'][0]
cve_changes.append({'action': 'INTRODUCED', 'cve_id': cve_id, 'name': affects['name'], 'version': affects['version'], 'cvssSeverity': finding['cvssSeverity'], 'cvssScore': finding['cvssScore']})

# Findings that were remediated
for finding in version1_findings:
cve_id = finding['cves'][0]['cveId']

if cve_id not in version2_cve_ids:
affects = finding['affects'][0]
cve_changes.append({'action': 'REMEDIATED', 'cve_id': cve_id, 'name': affects['name'], 'version': affects['version'], 'cvssSeverity': finding['cvssSeverity'], 'cvssScore': finding['cvssScore']})

return cve_changes


def compare_sw_components(sw_components_fw1, sw_components_fw2):
"""
Compares the software components between two asset versions
Expand Down Expand Up @@ -153,26 +197,44 @@ def main():

print("*" * 80)
version1_findings = finite_state_sdk.get_findings(token, ORGANIZATION_CONTEXT, asset_version_id=asset_version_id_1, category="CVE")
version1_cve_ids = [finding['cves'][0]['cveId'] for finding in version1_findings]
version2_findings = finite_state_sdk.get_findings(token, ORGANIZATION_CONTEXT, asset_version_id=asset_version_id_2, category="CVE")
version2_cve_ids = [finding['cves'][0]['cveId'] for finding in version2_findings]

print(f'CVEs Introduced')
cve_changes = compare_cves(version1_findings, version2_findings)

# Findings that were introduced
for cve_id in version2_cve_ids:
if cve_id not in version1_cve_ids:
# print in red
print(f"\033[91mCVE {cve_id} was introduced\033[0m")
# for grouping together
introduced_messages = []
remediated_messages = []

cve_changes_filename = f'{asset_version_1[0]["asset"]["name"]}-{asset_version_1[0]["name"]}-to-{asset_version_2[0]["name"]}-cve_changes.csv'
# replace filename spaces with underscores
cve_changes_filename = cve_changes_filename.replace(' ', '_')
with open(cve_changes_filename, 'w') as f:
# write header
f.write('action,cve_id,name,version,cvssSeverity,cvssScore\n')

for cve_change in cve_changes:
# write to csv
f.write(f"{cve_change['action']},{cve_change['cve_id']},{cve_change['name']},{cve_change['version']},{cve_change['cvssSeverity']},{cve_change['cvssScore']}\n")
if 'action' in cve_change:
if cve_change['action'] == 'INTRODUCED':
# print in green
introduced_messages.append(f"\033[91m{cve_change['cve_id']} was introduced for {cve_change['name']} {cve_change['version']} - [{cve_change['cvssSeverity']}] ({cve_change['cvssScore']})\033[0m")

elif cve_change['action'] == 'REMEDIATED':
# print in red
remediated_messages.append(f"\033[92m{cve_change['cve_id']} was remediated for {cve_change['name']} {cve_change['version']} - [{cve_change['cvssSeverity']}] ({cve_change['cvssScore']})\033[0m")

print(f'Wrote CVE changes to {cve_changes_filename}')

print("*" * 80)
print(f'CVEs Remediated')
print('CVEs INTRODUCED')
for message in introduced_messages:
print(message)

# Findings that were remediated
for cve_id in version1_cve_ids:
if cve_id not in version2_cve_ids:
# print in green
print(f"\033[92mCVE {cve_id} was remediated\033[0m")
print("*" * 80)
print('CVEs REMEDIATED')
for message in remediated_messages:
print(message)

version1_software_components = finite_state_sdk.get_software_components(token, ORGANIZATION_CONTEXT, asset_version_id=asset_version_id_1)
version2_software_components = finite_state_sdk.get_software_components(token, ORGANIZATION_CONTEXT, asset_version_id=asset_version_id_2)
Expand All @@ -185,17 +247,37 @@ def main():
updated_messages = []
removed_messages = []

for sbom_change in sw_changes:
if 'action' in sbom_change:
if sbom_change['action'] == 'UPDATED':
# print in yellow
updated_messages.append(f"\033[93m{sbom_change['name']} was updated from {sbom_change['version1']} to {sbom_change['version2']}\033[0m")
elif sbom_change['action'] == 'ADDED':
# print in green
added_messages.append(f"\033[92m{sbom_change['name']} was added with version {sbom_change['version2']}\033[0m")
elif sbom_change['action'] == 'REMOVED':
# print in red
removed_messages.append(f"\033[91m{sbom_change['name']} was removed with version {sbom_change['version1']}\033[0m")
sw_changes_filename = f'{asset_version_1[0]["asset"]["name"]}-{asset_version_1[0]["name"]}-to-{asset_version_2[0]["name"]}-sw_component_changes.csv'
# replace filename spaces with underscores
sw_changes_filename = sw_changes_filename.replace(' ', '_')
with open(sw_changes_filename, 'w') as f:
# write header
f.write('action,name,version1,version2\n')

for sw_change in sw_changes:
# write to csv
f.write(f"{sw_change['action']},{sw_change['name']}")
if "version1" in sw_change:
f.write(f",{sw_change['version1']}")
else:
f.write(",")
if "version2" in sw_change:
f.write(f",{sw_change['version2']}\n")
else:
f.write(",\n")

if 'action' in sw_change:
if sw_change['action'] == 'UPDATED':
# print in yellow
updated_messages.append(f"\033[93m{sw_change['name']} was updated from {sw_change['version1']} to {sw_change['version2']}\033[0m")
elif sw_change['action'] == 'ADDED':
# print in green
added_messages.append(f"\033[92m{sw_change['name']} was added with version {sw_change['version2']}\033[0m")
elif sw_change['action'] == 'REMOVED':
# print in red
removed_messages.append(f"\033[91m{sw_change['name']} was removed with version {sw_change['version1']}\033[0m")

print(f'Wrote Software Component changes to {sw_changes_filename}')

print("*" * 80)
print('Software Component Changes')
Expand Down
57 changes: 57 additions & 0 deletions examples/download_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import datetime
import finite_state_sdk
import requests


def custom_download_report(token, organization_context, report_type="CSV", report_subtype="ALL_FINDINGS", asset_version_id=None, output_filename=None):
"""
Demonstration of a method for getting a download URL.
Downloads a report from the Finite State Platform and saves it to the specified output_filename.
You could build your own method to do something else with the URL, or you can use the built-in finite_state_sdk.download_asset_version_report() method.
:param token: Finite State API token
:param organization_context: Finite State API organization context
:param report_type: The type of report to download. Valid values are "CSV" and "PDF"
:param report_subtype: The subtype of report to download. Valid values are "ALL_FINDINGS", "ALL_COMPONENTS", "EXPLOIT_INTELLIGENCE", and "RISK_SUMMARY". See API documentation for details.
:param asset_version_id: The asset version ID to download the SBOM for
:param output_filename: The filename to save the SBOM to
"""
url = finite_state_sdk.generate_report_download_url(token, organization_context, asset_version_id=asset_version_id, report_type=report_type, report_subtype=report_subtype, verbose=True) -> str:

# Send an HTTP GET request to the URL
response = requests.get(url)

# Check if the request was successful (status code 200)
if response.status_code == 200:
# Open a local file in binary write mode and write the content to it
print("File downloaded successfully.")
with open(output_filename, 'wb') as file:
file.write(response.content)
print(f'Wrote file to {output_filename}')
else:
print("Failed to download the file. Status code:", response.status_code)


def example_download_reports(token, organization_context):
# Download Reports for an asset version using custom method
asset_version_id = '123456789'
custom_download_report(token, organization_context, report_type="CSV", report_subttype="ALL_FINDINGS", asset_version_id=asset_version_id, output_filename=f'{asset_version_id}-all_findings.csv')

# Download Reports for an asset version using built-in SDK functions
downloads_folder = "downloads"
dt = datetime.datetime.now().strftime("%Y-%m-%d-%H%M")

# Download a CSV report of all findings for an asset version
finite_state_sdk.download_asset_version_report(token, organization_context, report_type="CSV", report_subtype="ALL_FINDINGS", asset_version_id='123456789', output_filename=f"{downloads_folder}/{dt}-artifact_version-all_findings.csv", verbose=True)
# Download a CSV report of all components for an asset version
finite_state_sdk.download_asset_version_report(token, organization_context, report_type="CSV", report_subtype="ALL_COMPONENTS", asset_version_id='123456789', output_filename=f"{downloads_folder}/{dt}-artifact_version-all_components.csv", verbose=True)
# Download a CSV report of all exploit intelligence for an asset version
finite_state_sdk.download_asset_version_report(token, organization_context, report_type="CSV", report_subtype="EXPLOIT_INTELLIGENCE", asset_version_id='123456789', output_filename=f"{downloads_folder}/{dt}-artifact_version-exploit_intelligence.csv", verbose=True)
# Download a PDF report of the risk summary for an asset version
finite_state_sdk.download_asset_version_report(token, organization_context, report_type="PDF", report_subtype="RISK_SUMMARY", asset_version_id='123456789', output_filename=f"{downloads_folder}/{dt}-artifact_version-risk_summary.pdf", verbose=True)

# Download Reports for a product

# Download a CSV report of all findings for a product
finite_state_sdk.download_product_report(token, organization_context, report_type="CSV", report_subtype="ALL_FINDINGS", product_id='123456789', output_filename=f"{downloads_folder}/{dt}-product-all_findings.csv", verbose=True)


82 changes: 82 additions & 0 deletions examples/generate_products_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# queries the API using the SDK to generate a CSV report for all Products in the Organization
import argparse
import datetime
import json
import os
from dotenv import load_dotenv

import sys
sys.path.append('..')
import finite_state_sdk

def main():
dt = datetime.datetime.now()
dt_str = dt.strftime("%Y-%m-%d-%H%M")

parser = argparse.ArgumentParser(description='Generate an CSV Products Report')
parser.add_argument('--secrets-file', type=str, help='Path to the secrets file', required=True)

args = parser.parse_args()

load_dotenv(args.secrets_file, override=True)

# get CLIENT_ID and CLIENT_SECRET from env
CLIENT_ID = os.environ.get("CLIENT_ID")
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
ORGANIZATION_CONTEXT = os.environ.get("ORGANIZATION_CONTEXT")

# Get an auth token - this is a bearer token that you will use for all subsequent requests
# The token is valid for 24 hours
token = finite_state_sdk.get_auth_token(CLIENT_ID, CLIENT_SECRET)

products = finite_state_sdk.get_all_products(token, ORGANIZATION_CONTEXT)
product_data = []

for product in products:
counts = {
'CRITICAL': 0,
'HIGH': 0,
'MEDIUM': 0,
'LOW': 0
}

product_name = product['name']

# get the default version for the assets in the product
for asset_version in product['assets']:
asset_version_id = str(asset_version['id'])

# get the count of findings for each severity
severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for severity in severities:
# do this because we can't in-line the findings meta query via the API, yet, and it requires several calls to get the counts
count = finite_state_sdk.get_findings(token, ORGANIZATION_CONTEXT, asset_version_id=asset_version_id, severity=severity, count=True)['count']
counts[severity] += count

product_data.append({
'product_name': product_name,
'relative_risk_score': product['relativeRiskScore'] if product['relativeRiskScore'] else 0,
'findings_critical': counts['CRITICAL'],
'findings_high': counts['HIGH'],
'findings_medium': counts['MEDIUM'],
'findings_low': counts['LOW'],
'artifact_count': len(product['assets']),
'business_unit': product['group']['name'],
'creator': f'{product["createdBy"]["email"]} {product["createdAt"]}'
})

# sort the product data by relative risk score
product_data = sorted(product_data, key=lambda k: k['relative_risk_score'], reverse=True)

# write to a csv file
filename = f'{dt_str}-product-report.csv'
with open(filename, 'w') as f:
f.write('product_name,relative_risk_score,findings_critical,findings_high,findings_medium,findings_low,artifact_count,business_unit,creator\n')
for product in product_data:
# format the relative risk score float to a string with 1 decimal places
product['relative_risk_score'] = f'{product["relative_risk_score"]:.1f}'
f.write(f"{product['product_name']},{product['relative_risk_score']},{product['findings_critical']},{product['findings_high']},{product['findings_medium']},{product['findings_low']},{product['artifact_count']},{product['business_unit']},{product['creator']}\n")
print(f'Wrote product report to {filename}')


main()
Loading

0 comments on commit c83a081

Please sign in to comment.