Spaces:
Runtime error
Runtime error
Download scribe_python_client/client.py from eporath/scribe-mcp: direct link, hf CLI and curl.
- Browser
- Download file 56.2 kB
-
https://huggingface.co/spaces/eporath/scribe-mcp/resolve/main/scribe_python_client/client.py
- Command line
-
hf download hf://spaces/eporath/scribe-mcp/scribe_python_client/client.py
-
curl -L -o client.py https://huggingface.co/spaces/eporath/scribe-mcp/resolve/main/scribe_python_client/client.py
56.2 kB
import os
import logging
import datetime
import json
import ast
import jwt
import urllib #Needed for avoiding using requests, which modified the S3 presigned URL
from urllib.parse import urlparse, parse_qs
import csv
import pathlib
from .utils import convert_timestamps, send_post_request, send_get_request, decode_jwt_token, querystr_to_json, att2statement
from .lineage_graph import create_lineage_graph
from .constants import (
SCRIBE_VULNERABILITIES_DATASET,
SCRIBE_PRODUCTS_DATASET,
SCRIBE_POLICY_DATASET,
SCRIBE_TEAM_STAT_DATASET,
SCRIBE_LINEAGE_DATASET,
SCRIBE_RISK_DATASET,
SCRIBE_FINDINGS_DATASET
)
class ScribeClient:
scribe_vulnerabilities_dataset = SCRIBE_VULNERABILITIES_DATASET
scribe_products_dataset = SCRIBE_PRODUCTS_DATASET
scribe_policy_dataset = SCRIBE_POLICY_DATASET
scribe_team_stat_dataset = SCRIBE_TEAM_STAT_DATASET
scribe_lineage_dataset=SCRIBE_LINEAGE_DATASET
scribe_risk_dataset = SCRIBE_RISK_DATASET
scribe_findings_dataset = SCRIBE_FINDINGS_DATASET
def get_chache_data(self):
# Exclude product_list from cache as per requirements
return {
"timestamp": datetime.datetime.now().isoformat(),
"datasets": self.datasets,
"team_name": self.team_name,
"team_info": self.team_info
}
def need_cache_data_update(self, cached_data):
# if timestamp older than one day
if cached_data and "timestamp" in cached_data:
try:
last_update = datetime.datetime.fromisoformat(cached_data["timestamp"])
except Exception as e:
logging.error(f"Error parsing timestamp: {e}")
return True
return (datetime.datetime.now() - last_update).days > 1
def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data=None, env = "prod",
username=None, password=None):
def update_from_cached_data(cached_data):
if "datasets" in cached_data:
self.datasets = cached_data["datasets"]
self.dataset_ids = self.create_dataset_id_map(self.datasets)
else:
logging.error("No datasets in provided cached data")
# product_list intentionally not loaded from cache
if "team_name" in cached_data:
self.team_name = cached_data["team_name"]
else:
logging.error("No team name in provided cached data")
if "team_info" in cached_data:
self.team_info = cached_data["team_info"]
else:
logging.error("No team info in provided cached data")
# Default cache file path
cache_dir = os.path.join(os.getcwd(), ".scribe")
cache_file = os.path.join(cache_dir, "cache.json")
loaded_cache = None
# Only load cache if not explicitly provided
if cached_data is None:
try:
if os.path.exists(cache_file):
# Check file age
file_mtime = datetime.datetime.fromtimestamp(os.path.getmtime(cache_file))
if (datetime.datetime.now() - file_mtime).days <= 1:
with open(cache_file, "r") as f:
loaded_cache = json.load(f)
else:
logging.info("Cache file is older than one day, will refresh cache.")
else:
# Ensure .scribe directory exists
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
except Exception as e:
logging.error(f"Error loading cache file: {e}")
else:
loaded_cache = cached_data
if ignore_env:
self.api_token = api_token
else:
self.api_token = api_token or os.getenv('SCRIBE_TOKEN', None)
if not self.api_token:
logging.error("No API token provided. Please set the SCRIBE_TOKEN environment variable or pass it as an argument.")
return
self.username = username
self.password = password
self.env = env
self.base_url = base_url
if env == "prod":
self.superset_base_url = "https://superset.scribesecurity.com"
else:
self.superset_base_url = f"https://internal-superset.{env}.scribesecurity.com"
self.refresh_interval = refresh_interval
self.last_refresh = datetime.datetime.now() - datetime.timedelta(days=5) # Initialize to a time in the past
self.jwt_token = None
self.superset_token = None
self.dataset_ids = None
self.datasets = None
self.product_list = None
self.team_id = None
self.team_name = None
self.team_info = None
p = None
if self.api_token:
self.last_refresh = datetime.datetime.now() # - datetime.timedelta(hours=5)
self.refresh_data(force=True)
# Use loaded_cache if available and not stale, else refresh and save
if not loaded_cache or self.need_cache_data_update(loaded_cache):
d = self.get_datasets()
if not d:
logging.error("Refresh: Failed to update dataset IDs")
else:
self.datasets = d
self.dataset_ids = self.create_dataset_id_map(d)
p = self.get_products(force=True)
if not p:
logging.error("Refresh: Failed to update product list")
logging.error(f"Using previous product list\n{self.product_list}")
else:
self.product_list = p
t = self.get_team_info()
if not t:
logging.error("Failed to get team data.")
# Save cache (excluding product_list)
try:
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
cache_data = self.get_chache_data()
with open(cache_file, "w") as f:
json.dump(cache_data, f, indent=2)
except Exception as e:
logging.error(f"Error saving cache file: {e}")
else:
update_from_cached_data(loaded_cache)
def validate_dataset(self, dataset_name):
if not self.dataset_ids:
self.refresh_data()
if not self.dataset_ids:
logging.error("Failed to get dataset IDs")
return False
if dataset_name not in self.dataset_ids:
logging.error(f"Dataset {dataset_name} not found")
return False
return True
def set_api_token(self, api_token):
if self.is_api_token_set():
return
self.api_token = api_token
if self.api_token:
self.last_refresh = datetime.datetime.now()
self.refresh_data(force=True)
def is_api_token_set(self):
return self.api_token is not None
def refresh_data(self, force=False):
if not self.api_token:
logging.error("No API token provided")
return
need_refresh = (
force or
not self.dataset_ids or
not self.product_list or
(datetime.datetime.now() - self.last_refresh).seconds > self.refresh_interval
)
if not need_refresh:
return
self.last_refresh = datetime.datetime.now()
self.jwt_token = self.login(self.api_token, self.base_url)
if not self.jwt_token:
logging.error("Refresh: Failed to get JWT token")
try:
decoded_token = jwt.decode(self.jwt_token, options={"verify_signature": False})
self.decoded_token = decoded_token
except jwt.InvalidTokenError as e:
logging.error(f"Invalid JWT token: {e}")
self.decoded_token = None
return
jwt_name = decoded_token.get("name")
try:
prefix = "scribe-hub-team" if self.env == "prod" else f"scribe-hub-{self.env}-team"
self.team_id = jwt_name.split(prefix)[1].split("-")[0]
logging.info(f"Team:{self.team_id}")
except Exception as e:
logging.error(f"Error extracting team ID: {e}")
self.team_id = None
self.superset_token = self.get_superset_token(self.jwt_token, self.base_url)
if not self.superset_token:
logging.error("Refresh: Failed to get Superset token")
def send_post_request(self, url, token=None, body=None):
"""
Deprecated: Use `send_post_request` from `utils.py` instead.
"""
return send_post_request(url, token, body)
def send_get_request(self, url, token=None):
"""
Deprecated: Use `send_get_request` from `utils.py` instead.
"""
return send_get_request(url, token)
def decode_jwt_token(self, jwt_token):
"""
Deprecated: Use `decode_jwt_token` from `utils.py` instead.
"""
return decode_jwt_token(jwt_token)
def querystr_to_json(self, query):
"""
Deprecated: Use `querystr_to_json` from `utils.py` instead.
"""
return querystr_to_json(query)
def att2statement(self, data):
"""
Deprecated: Use `att2statement` from `utils.py` instead.
"""
return att2statement(data)
def login(self, api_token, base_url):
url = f"{base_url}/v1/login"
body = {"api_token": api_token}
response = self.send_post_request(url=url, body=body)
if response and 'token' in response.json():
return response.json()['token']
else:
logging.error("Token not found in the response.")
return None
def get_superset_token(self, jwt_token, base_url):
url = f"{base_url}/dataset/token"
response = self.send_get_request(url=url, token=jwt_token)
if not response:
return None
response_json = json.loads(response.text)
if 'access_token' in response_json:
return response_json['access_token']
else:
logging.error("Access token not found in the response.")
return None
def create_dataset_id_map(self, datasets):
data = datasets
if not data:
return {}
try:
result = data.get("result", [])
datasource_id_map = {}
for entry in result:
datasource_name = entry.get("datasource_name")
datasource_id = entry.get("id")
if datasource_name and datasource_id:
datasource_id_map[datasource_name] = entry
return datasource_id_map
except json.JSONDecodeError as e:
logging.error(f"Error decoding JSON: {e}")
return {}
def get_team_info(self):
if not self.team_id:
logging.error("Team ID not set.")
return False
if not self.validate_dataset(self.scribe_team_stat_dataset):
return False
query = {
"columns": [
"teamName"
],
"filters": [
{
"col": "teamId",
"op": "==",
"val": self.team_id
}
],
"metrics": [],
"orderby": [],
"row_limit": 1
}
r = self.query_superset(self.scribe_team_stat_dataset, query)
try:
self.team_name = r["result"][0]["data"][0]["teamName"]
self.team_info = r["result"][0]["data"][0]
except (KeyError, IndexError) as e:
logging.error(f"Error accessing response data: {e}")
return False
except json.JSONDecodeError as e:
logging.error(f"Error decoding JSON: {e}")
return False
return True
def get_link(self, use = "products", text = "Link", params = {}):
params_str = ""
if params:
# %3B is the URL encoded version of ';'
params_str = ";;".join([f"{k}:{v}" for k, v in params.items()])
params_str = "searchFilters=" + params_str
if use == "products":
return f"\n[{text}](https://scribe-security.github.io/redirect/redirect.html?path=%2Fproducer-products)"
# Add here elif for all uses
elif use == "product_vulnerabilities":
if params:
params_str = '&' + params_str
# return f"\n[{text}](http://localhost:8090/redirect.html?path=%2Fsbom?redirectTabName=Vulnerabilities&{params_str})"
return f"\n[{text}](https://scribe-security.github.io/redirect/redirect.html?path=%2Fsbom?redirectTabName=Vulnerabilities{params_str})"
elif use == "product_policy_results":
if params:
params_str = '?' + params_str
return f"\n[{text}](https://scribe-security.github.io/redirect/redirect.html?path=%2Fpolicy%2Fevaluation{params_str})"
else:
return f"\n[{text}](https://app.scribesecurity.com)"
def get_products(self, all_versions=False, force=False):
if not force:
if (datetime.datetime.now() - self.last_refresh).seconds < self.refresh_interval and self.product_list:
return self.product_list
if not self.validate_dataset(self.scribe_products_dataset):
return None
url = f"{self.base_url}/dataset/data"
body = {
"superset_token": self.superset_token,
"validate": False,
"query": {
"datasource": {
"id": self.dataset_ids[self.scribe_products_dataset]["id"],
"type": "table"
},
"force": "false",
"queries": [
{
"columns": [
"logical_app",
"logical_app_version",
"version_timestamp"
],
"filters": [],
"metrics": [],
"orderby": [["logical_app", True], ["version_timestamp", False]],
"row_limit": 0
}
],
"result_format": "json",
"result_type": "results"
}
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if not r:
return self.product_list
try:
o = r.json()
o = convert_timestamps(o, "version_timestamp")
result = []
for product in o["result"][0]["data"]:
item = {"name": product["logical_app"], "version": product["logical_app_version"], "timestamp": product["version_timestamp"]}
result.append(item)
self.product_list = result
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return self.product_list
if all_versions:
return result
# Keep only latest version per product
latest = []
last_product = ""
for product in result:
if product['name'] == last_product:
continue
last_product = product['name']
latest.append(product)
# sort by timestamp
latest.sort(key=lambda x: x['timestamp'], reverse=True)
return latest
def format_table(self, headers, rows):
"""
Format a table with aligned columns for better readability in plain text.
Parameters:
headers (list): List of column headers.
rows (list of lists): List of rows, where each row is a list of column values.
Returns:
str: Formatted table as a string.
"""
# Calculate the maximum width of each column
column_widths = [len(header) for header in headers]
for row in rows:
for i, value in enumerate(row):
column_widths[i] = max(column_widths[i], len(str(value)))
# Create a format string for each row
row_format = " | ".join(f"{{:<{width}}}" for width in column_widths)
# Format the header and rows
header_line = row_format.format(*headers)
separator_line = "-+-".join("-" * width for width in column_widths)
row_lines = [row_format.format(*[str(value) for value in row]) for row in rows]
# Combine all parts into a single string
return "\n".join([header_line, separator_line] + row_lines)
def format_query_result_table(self, query_result):
"""
Format query results into an aligned table for better readability.
Parameters:
query_result (dict): The query result containing columns and data.
Returns:
str: Formatted table as a string.
"""
try:
data = query_result["result"][0]["data"]
if not data:
return "No data found"
columns = query_result["result"][0]["colnames"]
if not columns:
return "No columns information found"
# Prepare headers and rows for the table
headers = columns
rows = [[row[col] for col in columns] for row in data]
# Format the table using the existing format_table method
return self.format_table(headers, rows)
except Exception as e:
logging.error(f"Error formatting query result: {e}")
return "Error formatting query result"
def get_products_str(self, with_link=True):
r = self.get_products()
if not r:
return "No products found"
try:
# Prepare headers and rows for the table
headers = ["App", "Version", "Version Timestamp"]
rows = [[row['name'], row['version'], row['timestamp']] for row in r]
# Format the table
table = self.format_table(headers, rows)
if with_link:
table += self.get_link(use="products", text="Product page")
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
table = "Error getting product list"
logging.info(table)
return table
def get_logical_app_version(self, logical_app):
if not self.product_list:
self.refresh_data()
if not self.product_list:
return "I have issues getting data from Scribe, Sorry"
for product in self.product_list:
if product["name"] == logical_app:
latest = product["version"]
logging.info(f"Latest version for {logical_app} is {latest}")
return latest
return None
def get_product_vulnerabilities(self, logical_app, logical_app_version=None):
self.refresh_data()
if not logical_app_version:
v = self.get_logical_app_version(logical_app)
if v:
logical_app_version = v
else:
logging.error(f"Failed to get version for {logical_app}")
return None, None
if not self.validate_dataset(self.scribe_vulnerabilities_dataset):
return None
url = f"{self.base_url}/dataset/data"
body = {
"superset_token": self.superset_token,
"validate": False,
"query": {
"datasource": {
"id": self.dataset_ids[self.scribe_vulnerabilities_dataset]["id"],
"type": "table"
},
"force": False,
"queries": [
{
"columns": [
"vulnerability_id",
"severity",
"epssProbability",
"targetName",
"component_name",
"logical_app",
],
"filters": [
{
"col": "logical_app",
"op": "==",
"val": logical_app
},
{
"col": "logical_app_version",
"op": "==",
"val": logical_app_version
},
{
"col": "vulnerability_id",
"op": "like",
"val": "CVE-%"
}
],
"metrics": [],
"orderby": [["severity", False], ["epssProbability", False]],
"row_limit": 10
}
],
"result_format": "json",
"result_type": "results"
},
"validate": False
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
try:
return r.json(), logical_app_version
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return {}, logical_app_version
return {}, logical_app_version
def get_product_vulnerabilities_str(self, logical_app, logical_app_version=None, with_link=True):
r, logical_app_version = self.get_product_vulnerabilities(logical_app, logical_app_version)
if not r:
return "Failed getting vulnerability data"
try:
vuln_list = r["result"][0]["data"]
if not vuln_list:
return "No vulnerabilities found"
# Prepare headers and rows for the table
headers = ["ID", "Severity", "EPSS", "Vulnerable Component", "Artifact"]
rows = [
[
f"[{vuln['vulnerability_id']}](https://nvd.nist.gov/vuln/detail/{vuln['vulnerability_id']})",
vuln['severity'],
vuln['epssProbability'],
vuln['component_name'],
vuln['targetName']
]
for vuln in vuln_list
]
# Format the table
table = self.format_table(headers, rows)
if with_link:
table += self.get_link(
use="product_vulnerabilities",
text="Product vulnerabilities",
params={"product": logical_app, "product_version": logical_app_version, "show_file_components": "false"}
)
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
table = "Error getting vulnerability list"
logging.info(table)
return table
def get_product_vulnerability_distribution(self, logical_app, logical_app_version=None):
def severity_label(severity):
print(f"Assigning severity label for severity: {severity}")
if severity <= 3.9:
return "Low"
elif severity <= 6.9:
return "Medium"
elif severity <= 8.9:
return "High"
else:
return "Critical"
self.refresh_data()
if not logical_app_version:
v = self.get_logical_app_version(logical_app)
if v:
logical_app_version = v
else:
return None
if not self.validate_dataset(self.scribe_vulnerabilities_dataset):
return None
url = f"{self.base_url}/dataset/data"
body = {
"superset_token": self.superset_token,
"validate": False,
"query": {
"datasource": {
"id": self.dataset_ids[self.scribe_vulnerabilities_dataset]["id"],
"type": "table"
},
"force": False,
"queries": [
{
"columns": [
"logical_app",
"logical_app_version",
"vulnerability_id",
"severity"
],
"filters": [
{
"col": "logical_app",
"op": "==",
"val": logical_app
},
{
"col": "logical_app_version",
"op": "==",
"val": logical_app_version
},
{
"col": "vulnerability_id",
"op": "like",
"val": "CVE-%"
}
],
"metrics": [
{
"label": "vulnerabilities",
"expressionType": "SQL",
"sqlExpression": "COUNT(DISTINCT vulnerability_id)"
},
],
"post_processing": [],
"groupby": ["severity"],
"orderby": [["severity", False]],
"row_limit": 10
}
],
"result_format": "json",
"result_type": "results"
}
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
try:
r = r.json()
data = r["result"][0]["data"]
for row in data:
row["severity_label"] = severity_label(row["severity"])
return data
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return []
return []
def get_datasets(self):
url = f"{self.base_url}/dataset"
body = {
"superset_token": self.superset_token
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
r = r.json()
os.makedirs("tmp", exist_ok=True) # Ensure the tmp directory exists
with open("tmp/datasets.json", "w") as f:
json.dump(r, f)
return r
def query_superset(self, dataset, query, time_tokens=[]):
dataset_name = dataset
id = None
if self.dataset_ids and dataset in self.dataset_ids:
id = self.dataset_ids[dataset]["id"]
else:
logging.error(f"Dataset {dataset} not found in dataset IDs")
return None
url = f"{self.base_url}/dataset/data"
body = {
"superset_token": self.superset_token,
"validate": False,
"query": {
"datasource": {
"id": id,
"type": "table"
},
"force": "false",
"queries": [
query
],
"result_format": "json",
"result_type": "results"
},
"validate": False
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
try:
r = r.json()
for t in time_tokens:
r = convert_timestamps(r, col_substr=t)
return r
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return {}
def query_result_to_str(self, header, r):
if not header:
header = ""
try:
data = r["result"][0]["data"]
if not data:
return "No data found"
columns = r["result"][0]["colnames"]
if not columns:
return "No columns information found"
md_table = ["| " + " | ".join(columns) + " |", "| ---" * len(columns) + " |"]
for row in data:
md_table.append("| " + " | ".join([str(row[col]) for col in columns]) + " |")
o = header + '\n' + "\n".join(md_table)
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
o = "Error getting data"
return o
def query_str(self, dataset, query, header = "Query results:"):
r = self.query_superset(dataset, query, time_tokens = ["timestamp", "time_evaluated", "Time", "created", "published_on", "LastModified",
"date_changed", "advisory_modified"])
if not r:
return "Failed getting data"
o = self.query_result_to_str(header,r)
return o
def get_policy_results(self, logical_app="Scribot", logical_app_version=None, initiative="%"):
self.refresh_data()
if not logical_app_version:
v = self.get_logical_app_version(logical_app)
if v:
logical_app_version = v
else:
logging.error(f"Failed to get version for {logical_app}")
return None, None
if not self.validate_dataset(self.scribe_policy_dataset):
return None, None
url = f"{self.base_url}/dataset/data"
body = {
"superset_token": self.superset_token,
"validate": False,
"query": {
"datasource": {
"id": self.dataset_ids[self.scribe_policy_dataset]["id"],
"type": "table"
},
"force": "false",
"queries": [
{
"columns": [
"logical_app",
"logical_app_version",
"asset_type",
"asset_name",
"status",
"time_evaluated",
"initiative_name",
"control_name",
"rule_name",
"message"
],
"filters": [
# {
# "col": "initiative_name",
# "op": "like",
# "val": initiative
# },
{
"col": "logical_app",
"op": "==",
"val": logical_app
},
{
"col": "logical_app_version",
"op": "==",
"val": logical_app_version
},
{
"col": "status",
"op": "not in",
"val": ['Not Applicable', 'Not Applicable']
}
],
"metrics": [],
"orderby": [],#["time_evaluated", False], ["status", True]],
"row_limit": 10
}
],
"result_format": "json",
"result_type": "results"
}
}
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
try:
r = r.json()
r = convert_timestamps(r, "time_evaluated")
return r, logical_app_version
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return {}, logical_app_version
return {}, logical_app_version
def get_policy_results_str(self, logical_app="scribe-platform", logical_app_version=None, initiative="%", with_link = True):
r, logical_app_version = self.get_policy_results(logical_app, logical_app_version, initiative)
if not r:
return "Failed getting policy results"
o = f"Policy results for {logical_app}:\n"
try:
o = self.query_result_to_str(o, r)
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
o = "Error getting policy results"
return o
def get_product_lineage(self, logical_app, logical_app_version=None):
self.refresh_data()
if not logical_app_version:
v = self.get_logical_app_version(logical_app)
if v:
logical_app_version = v
else:
logging.error(f"Failed to get version for {logical_app}")
return None, None
if not self.validate_dataset(self.scribe_policy_dataset):
return None, None
# left out columns:
# "product_id",
# "version_id",
# "child_id",
# "properties",
query = {
"columns": [
"timestamp",
"logical_app",
"logical_app_version",
"platform_name",
"platform_type",
"asset_type",
"asset_name",
"external_id",
"uri",
"owner",
"path",
"parent_id",
"parent_type",
"parent_name",
"parent_external_id",
# "properties"
],
"filters": [
{
"col": "logical_app",
"op": "==",
"val": logical_app
},
{
"col": "logical_app_version",
"op": "==",
"val": logical_app_version
}
],
"metrics": [],
"orderby": [["timestamp", False]],
"annotation_layers": [],
"row_limit": 100,
"post_processing": []
}
r = self.query_superset(self.scribe_lineage_dataset, query, time_tokens = ["timestamp"])
# TODO: check in superset prod for more columns
if r:
try:
data = r["result"][0]["data"]
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return [], logical_app_version
if not data:
return [], logical_app_version
for row in data:
if row.get("properties"):
try:
row["properties"] = ast.literal_eval(row["properties"])
except Exception as e:
logging.error(f"Error decoding properties: {e}")
row["properties"] = {}
return data, logical_app_version
else:
return [], logical_app_version
types_to_position = {
"organization": 1,
"repo": 2,
"branch": 3,
"workflow": 4,
"workflow_run": 5,
"image": 6,
"pod": 7,
"namespace": 8
}
include_fields = [
"platform_name", "platform_type",
"asset_name", "asset_type","parent_name", "parent_type"
]
def filter_lineage_data(self, data, include_types=types_to_position, include_fields=include_fields):
"""
Filter the data to only include records of the specified types
and remove specified fields.
Parameters:
data (list): List of JSON records.
include_types (set): Asset types to keep.
include_fields (set): Fields to remove from records.
Returns:
list: Filtered data.
"""
filtered_data = []
for record in data:
if record.get("asset_type") in include_types:
# Create a copy to avoid modifying original data
filtered_record = {key: value for key, value in record.items() if key in include_fields}
filtered_data.append(filtered_record)
return filtered_data
def list_attestations(self, criteria={}):
self.refresh_data()
url = f"{self.base_url}/evidence/list"
body = criteria
r = self.send_post_request(url=url, token=self.jwt_token, body=body)
if r:
try:
return r.json()
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return {}
def get_attestation(self, attestation_id):
url = f"{self.base_url}/evidence/{attestation_id}"
r = self.send_get_request(url=url, token=self.jwt_token)
if not r:
return {}
try:
o = r.json()
url = o.get("presigned_url")
if url:
response = urllib.request.urlopen(url)
content = response.read()
o = json.loads(content)
if 'payload' in o:
o = self.att2statement(o)
else:
logging.error(f"Presigned URL not found in response")
return {}
except Exception as e:
logging.error(f"Error decoding JSON: {e}")
return {}
return o
def get_latest_attestation(self, criteria={}):
atts = self.list_attestations(criteria=criteria)
if not atts:
return {}
evidence_list = atts.get("evidences", [])
if not evidence_list:
return {}
evidence_list.sort(key=lambda x: x['context']["timestamp"], reverse=True)
if not evidence_list:
return {}
evidence = evidence_list[0]
att = self.get_attestation(evidence["id"])
return att
def query_vulnerabilities(self, querystr, title="") -> str:
logging.debug(f"Query vulnerabilities with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_vulnerabilities_dataset, query, time_tokens=["timestamp"])
if r:
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def query_products(self, querystr, title="") -> str:
logging.debug(f"Query products with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_products_dataset, query, time_tokens=["timestamp"])
if r:
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def get_lineage_graph(self, lineage_data, output_file):
"""
Generate a lineage graph from the lineage data and save it to the specified output file.
Parameters:
lineage_data (list): List of lineage records.
output_file (str): Path to save the generated graph.
Returns:
str: Full path of the saved graph file.
"""
if not lineage_data:
logging.error("No lineage data provided for graph generation.")
return "Error: No lineage data provided."
full_path_filename = create_lineage_graph(lineage_data, output_file=output_file)
return full_path_filename
def query_lineage(self, querystr, title="", graph_filename=None) -> str:
logging.debug(f"Query lineage with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_lineage_dataset, query, time_tokens=["timestamp"])
if r:
if graph_filename:
required_fields = ["asset_name", "asset_type", "parent_name", "parent_type", "external_id", "parent_external_id", "uri"]
columns = query.get("columns", [])
if not all(field in columns for field in required_fields):
logging.error(f"Query must include the following fields: {', '.join(required_fields)}")
return "Error: Query must include required fields for lineage graph generation."
lineage_data = r["result"][0]["data"]
full_path_filename = create_lineage_graph(lineage_data, output_file=graph_filename)
logging.info(f"Lineage graph saved to {full_path_filename}")
else:
logging.warning("No graph filename provided, lineage graph will not be generated.")
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def query_policy_results(self, querystr, title="") -> str:
logging.debug(f"Query policy results with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_policy_dataset, query, time_tokens=["time_evaluated"])
if r:
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def query_risk(self, querystr, title="") -> str:
logging.debug(f"Query risk table with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_risk_dataset, query, time_tokens=["timestamp"])
if r:
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def query_findings(self, querystr, title="") -> str:
logging.debug(f"Query finding table with: {querystr}")
query = self.querystr_to_json(querystr)
if query:
r = self.query_superset(self.scribe_findings_dataset, query, time_tokens=["timestamp"])
if r:
return f"{title}\n{self.format_query_result_table(r)}"
return "Error running query"
def superset_authenticate(self):
if not self.username or not self.password:
logging.error("Username and password must be provided for Superset login.")
return False
login_url = f"{self.superset_base_url}/api/v1/security/login"
body = {
"username": self.username,
"password": self.password,
"provider": "db",
"refresh": True
}
response = self.send_post_request(login_url, token = None, body=body)
if not response:
logging.error("Login failed. No response received.")
return False
try:
result = response.json()
except json.JSONDecodeError:
logging.error("Failed to parse login response.")
return False
access_token = result.get("access_token")
if access_token:
self.jwt_token = access_token
self.superset_token = access_token
logging.info("Login successful. Tokens obtained.")
return True
else:
logging.error("Login failed. No access token found.")
return False
def get_dataset_links(self):
self.superset_authenticate()
if not self.jwt_token:
logging.error("No JWT token found. Please authenticate first.")
return []
dataset_links = []
#Use slug to directly fetch the API dashboard's datasets
dashboard_slug = "api-dashboard"
datasets_url = f"{self.superset_base_url}/api/v1/dashboard/{dashboard_slug}/datasets"
logging.info(f"Fetching datasets from {datasets_url}")
datasets_response = self.send_get_request(datasets_url, token=self.jwt_token)
if not datasets_response:
logging.error("Failed to fetch datasets for API dashboard.")
return []
try:
datasets_data = datasets_response.json()
datasets = datasets_data.get("result", [])
for dataset in datasets:
dataset_id = dataset.get("id")
if dataset_id:
link = f"{self.base_url}/explore/?datasource_type=table&datasource_id={dataset_id}"
dataset_links.append(link)
except json.JSONDecodeError as e:
logging.error(f"Error parsing dataset response: {e}")
logging.info(f"Total dataset links collected: {len(dataset_links)}")
return dataset_links
def get_dataset_data(self, dataset_links, output_file="dataset_metadata.csv"):
records = []
for link in dataset_links:
#Properly parse dataset ID from the URL
parsed_url = urlparse(link)
query_params = parse_qs(parsed_url.query)
dataset_id_list = query_params.get("datasource_id")
if not dataset_id_list:
logging.error(f"Could not find dataset_id in link: {link}")
continue
dataset_id = dataset_id_list[0]
dataset_url = f"{self.superset_base_url}/api/v1/dataset/{dataset_id}"
logging.info(f"Fetching metadata from {dataset_url}")
response = self.send_get_request(dataset_url, token=self.jwt_token)
if not response:
logging.error(f"Failed to fetch metadata for dataset ID {dataset_id}")
continue
try:
metadata_json = response.json()
dataset_metadata = metadata_json.get("result", {})
except json.JSONDecodeError as e:
logging.error(f"Failed to parse metadata JSON for dataset ID {dataset_id}: {e}")
continue
verbose_name = dataset_metadata.get("verbose_name")
if not verbose_name or verbose_name == dataset_metadata.get("table_name"):
verbose_name = dataset_metadata.get("table_name", "").replace("_", " ").title()
#Get dataset metadata
dataset_info = {
"dataset_id": dataset_metadata.get("id"),
"catalog": dataset_metadata.get("catalog"),
"changed_on": dataset_metadata.get("changed_on"),
"table_name": dataset_metadata.get("table_name"),
"verbose_name": verbose_name,
"schema": dataset_metadata.get("schema"),
"datasource_type": dataset_metadata.get("datasource_type"),
"description": dataset_metadata.get("description"),
}
#Get column metadata
columns = dataset_metadata.get("columns", [])
if columns:
for col in columns:
record = dataset_info.copy()
record.update({
"column_id": col.get("id"),
"column_name": col.get("column_name"),
"column_verbose_name": col.get("verbose_name") or col.get("column_name", "").replace("_", " ").title(),
"column_type": col.get("type"),
"groupby": col.get("groupby"),
"filterable": col.get("filterable"),
"is_dttm": col.get("is_dttm"),
"column_description": col.get("description"),
})
records.append(record)
else:
records.append(dataset_info)
#Write to CSV
if records:
fieldnames = [
"dataset_id", "table_name", "verbose_name", "schema", "catalog",
"datasource_type", "changed_on", "description",
"column_id", "column_name", "column_verbose_name", "column_type",
"groupby", "filterable", "is_dttm", "column_description"
]
fieldnames = [field for field in fieldnames if any(field in rec for rec in records)]
with open(output_file, mode='w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(records)
logging.info(f"Dataset metadata exported to {output_file}")
else:
logging.warning("No dataset metadata exported.")
def get_dataset_tables_dict(self, tables=[]) -> dict:
if not self.jwt_token:
self.superset_authenticate()
dataset_links = self.get_dataset_links()
if not dataset_links:
logging.error("No dataset links found.")
return {}
self.get_dataset_data(dataset_links)
# Load data from the CSV file (generated by get_dataset_data)
records = []
try:
with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
records.append(row)
except Exception as e:
logging.error(f"Failed to load dataset_metadata.csv: {e}")
return {}
# Group column data by table name
table_dict = {}
for row in records:
table_name = row.get("table_name")
if tables and table_name not in tables:
continue
if table_name not in table_dict:
table_dict[table_name] = []
table_dict[table_name].append({
"column_name": row.get("column_name", ""),
"type": row.get("column_type", ""),
"label": row.get("column_verbose_name") or row.get("column_name", "").replace("_", " ").title(),
"description": row.get("column_description", "")
})
# Add markdown for each table
for table_name, cols in table_dict.items():
md_parts = []
md_parts.append(f"## `{table_name}` Columns\n\n")
md_parts.append("| Column Name | Label | Type | Description | Like-compliant |\n")
md_parts.append("|-------------|-------|------|-------------|----------------|\n")
for col in cols:
column_name = col["column_name"]
label = col["label"]
col_type = col["type"].upper()
description = col["description"]
expr = row.get("expression", "") or ""
if col_type == "STRING" and (" in (" in expr.lower() or " in(" in expr.lower()):
like_compliant = False
else:
like_compliant = True
md_parts.append(
f"| `{column_name}` | {label} | {col_type} | {description} | {str(like_compliant)} |\n"
)
md_parts.append("\n")
md_str = "".join(md_parts)
# Add markdown string to the dict for this table
# If table_dict[table_name] is a list, convert to dict with 'columns' and 'md'
table_dict[table_name] = {
"columns": cols,
"md": md_str
}
return table_dict
def get_dataset_tables_md(self, tables=[]) -> str:
if not self.jwt_token:
self.superset_authenticate()
dataset_links = self.get_dataset_links()
if not dataset_links:
logging.error("No dataset links found.")
return ""
self.get_dataset_data(dataset_links)
# Load data from the CSV file
records = []
try:
with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
records.append(row)
except Exception as e:
logging.error(f"Failed to load dataset_metadata.csv: {e}")
return ""
# Group columns by table_name
table_columns = {}
for row in records:
table_name = row.get("table_name")
if tables and table_name not in tables:
continue
if table_name not in table_columns:
table_columns[table_name] = []
column_name = row.get("column_name", "")
col_type = row.get("column_type", "")
col_type = row.get("column_type", "").upper()
expr = row.get("expression", "") or ""
if col_type == "STRING" and (" in (" in expr.lower() or " in(" in expr.lower()):
like_compliant = False
else:
like_compliant = True
table_columns[table_name].append({
"column_name": column_name,
"label": row.get("column_verbose_name") or column_name.replace("_", " ").title(),
"type": col_type,
"description": row.get("column_description", ""),
"like_compliant": str(like_compliant)
})
# Build markdown
md_parts = []
for table_name, cols in table_columns.items():
md_parts.append(f"## `{table_name}` Columns\n\n")
md_parts.append("| Column Name | Label | Type | Description | Like-compliant |\n")
md_parts.append("|-------------|-------|------|-------------|----------------|\n")
for col in cols:
md_parts.append(
f"| `{col['column_name']}` | {col['label']} | {col['type']} | "
f"{col['description']} | {col['like_compliant']} |\n"
)
md_parts.append("\n")
markdown_output = "".join(md_parts)
# Write to file
with open("dataset_tables.md", "w", encoding="utf-8") as f:
f.write(markdown_output)
logging.info("Saved table metadata Markdown to dataset_tables.md")
return markdown_output
def get_dataset_prompt(self, dataset, prompt_template=None):
"""
Returns a prompt string for the given dataset, using a template.
- dataset: table name to fetch from docs/dataset_tables.json
- prompt_template: template string with '{table}' placeholder, or None to load from docs/<dataset>-template.md
"""
import os
import json
# Load table data from docs/dataset_tables.json
table_json_path = os.path.join("docs", "dataset_tables.json")
if not os.path.exists(table_json_path):
raise FileNotFoundError(f"dataset_tables.json not found at {table_json_path}")
with open(table_json_path, "r", encoding="utf-8") as f:
tables = json.load(f)
if dataset not in tables:
raise ValueError(f"Dataset '{dataset}' not found in dataset_tables.json")
table_md = tables[dataset].get("md")
if not table_md:
raise ValueError(f"No markdown found for dataset '{dataset}' in dataset_tables.json")
# Load template
if prompt_template is None:
template_filename = f"{dataset.replace(' ', '_')}-template.md"
template_path = os.path.join("docs", template_filename)
if os.path.exists(template_path):
with open(template_path, "r", encoding="utf-8") as f:
prompt_template = f.read()
else:
prompt_template = "{table}"
# Build prompt
prompt = prompt_template.replace("{table}", table_md)
return prompt
import os
import logging
import datetime
import json
import ast
import jwt
import urllib #Needed for avoiding using requests, which modified the S3 presigned URL
from urllib.parse import urlparse, parse_qs
import csv
import pathlib
from .utils import convert_timestamps, send_post_request, send_get_request, decode_jwt_token, querystr_to_json, att2statement
from .lineage_graph import create_lineage_graph
from .constants import (
SCRIBE_VULNERABILITIES_DATASET,
SCRIBE_PRODUCTS_DATASET,
SCRIBE_POLICY_DATASET,
SCRIBE_TEAM_STAT_DATASET,
SCRIBE_LINEAGE_DATASET,
SCRIBE_RISK_DATASET,
SCRIBE_FINDINGS_DATASET
)
class ScribeClient:
scribe_vulnerabilities_dataset = SCRIBE_VULNERABILITIES_DATASET
scribe_products_dataset = SCRIBE_PRODUCTS_DATASET
scribe_policy_dataset = SCRIBE_POLICY_DATASET
scribe_team_stat_dataset = SCRIBE_TEAM_STAT_DATASET
scribe_lineage_dataset=SCRIBE_LINEAGE_DATASET
scribe_risk_dataset = SCRIBE_RISK_DATASET
scribe_findings_dataset = SCRIBE_FINDINGS_DATASET
def get_chache_data(self):
# Exclude product_list from cache as per requirements
return {
"timestamp": datetime.datetime.now().isoformat(),
"datasets": self.datasets,
"team_name": self.team_name,
"team_info": self.team_info
}
def need_cache_data_update(self, cached_data):
# if timestamp older than one day
if cached_data and "timestamp" in cached_data:
try:
last_update = datetime.datetime.fromisoformat(cached_data["timestamp"])
except Exception as e:
logging.error(f"Error parsing timestamp: {e}")
return True
return (datetime.datetime.now() - last_update).days > 1
def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data=None, env = "prod",
username=None, password=None):
def update_from_cached_data(cached_data):
if "datasets" in cached_data:
self.datasets = cached_data["datasets"]
self.dataset_ids = self.create_dataset_id_map(self.datasets)
else:
logging.error("No datasets in provided cached data")
# product_list intentionally not loaded from cache
if "team_name" in cached_data:
self.team_name = cached_data["team_name"]
else:
logging.error("No team name in provided cached data")
if "team_info" in cached_data:
self.team_info = cached_data["team_info"]
else:
logging.error("No team info in provided cached data")
# Default cache file path
cache_dir = os.path.join(os.getcwd(), ".scribe")
cache_file = os.path.join(cache_dir, "cache.json")
loaded_cache = None
# Only load cache if not explicitly provided
if cached_data is None:
try:
if os.path.exists(cache_file):
# Check file age
file_mtime = datetime.datetime.fromtimestamp(os.path.getmtime(cache_file))
if (datetime.datetime.now() - file_mtime).days <= 1:
with open(cache_file, "r") as f:
loaded_cache = json.load(f)
else:
logging.info("Cache file is older than one day, will refresh cache.")
else:
# Ensure .scribe directory exists
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
except Exception as e:
logging.error(f"Error loading cache file: {e}")
else:
loaded_cache = cached_data
if ignore_env:
self.api_token = api_token
else:
self.api_token = api_token or os.getenv('SCRIBE_TOKEN', None)