add nocolyapi v0.1

This commit is contained in:
funnywolf
2025-09-23 19:30:21 +08:00
parent 460ad4de39
commit 712d2219ae
4 changed files with 486 additions and 0 deletions
Binary file not shown.
+349
View File
@@ -0,0 +1,349 @@
from typing import TypedDict, Literal, List, Union, Any
import requests
from CONFIG import NOCOLY_URL, AISOAR_APPKEY, AISOAR_SIGN
class Field(TypedDict):
id: str
name: str
alias: str
value: str
desc: str
type: str
required: bool
isHidden: bool
isReadOnly: bool
isHiddenOnCreate: bool
isUnique: bool
isTitle: bool
remark: str
class Option(TypedDict):
key: str
value: str
index: int
score: float
# Define the recursive types first
class Condition(TypedDict):
type: Literal["condition"]
field: str
operator: str
value: Any
class Group(TypedDict):
type: Literal["group"]
logic: Literal["AND", "OR"]
children: List[Union["Group", Condition]]
class Worksheet(object):
def __init__(self):
pass
@staticmethod
def get_fields(worksheet_id: str) -> List[Field]:
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/worksheets/{worksheet_id}"
response = requests.get(
url,
params={"includeSystemFields": True},
headers=headers
)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data").get("fields")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')}")
class WorksheetRow(object):
def __init__(self):
pass
@staticmethod
def get(worksheet_id: str, row_id: str):
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/worksheets/{worksheet_id}/rows/{row_id}"
try:
response = requests.get(
url,
params={"includeSystemFields": True},
headers=headers
)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')}")
except Exception as e:
raise
@staticmethod
def list(worksheet_id: str, filter: dict):
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/worksheets/{worksheet_id}/rows/list"
data = {
"filter": filter,
"sorts": [
{
"field": "utime",
"direction": "desc"
}
],
"includeTotalCount": True,
"includeSystemFields": True,
"useFieldIdAsKey": False,
}
try:
response = requests.post(url,
headers=headers,
json=data)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data").get("rows")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')}")
except Exception as e:
raise
@staticmethod
def create(worksheet_id: str, fields: list):
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/worksheets/{worksheet_id}/rows"
data = {
"triggerWorkflow": True,
"fields": fields
}
try:
response = requests.post(url,
headers=headers,
json=data)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data").get("id")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')} data: {response_data.get('data')}")
except Exception as e:
raise
@staticmethod
def update(worksheet_id: str, row_id: str, fields: list):
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/worksheets/{worksheet_id}/rows/{row_id}"
data = {
"triggerWorkflow": True,
"fields": fields
}
try:
response = requests.patch(url,
headers=headers,
json=data)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')}")
except Exception as e:
raise
class OptionSet(object):
def __init__(self):
pass
@staticmethod
def list():
headers = {"HAP-Appkey": AISOAR_APPKEY,
"HAP-Sign": AISOAR_SIGN}
url = f"{NOCOLY_URL}/api/v3/app/optionsets"
response = requests.get(url,
headers=headers)
response.raise_for_status()
response_data = response.json()
if response_data.get("success"):
return response_data.get("data").get("optionsets")
else:
raise Exception(f"error_code: {response_data.get('error_code')} error_msg: {response_data.get('error_msg')}")
@staticmethod
def get(name):
optionsets = OptionSet.list()
for optionset in optionsets:
if optionset["name"] == name:
return optionset
raise Exception(f"optionset {name} not found")
@staticmethod
def get_option_by_name_and_value(name, value) -> Option:
optionsets = OptionSet.list()
for optionset in optionsets:
if optionset["name"] == name:
options = optionset.get("options", [])
for option in options:
if option["value"] == value:
return option
raise Exception(f"optionset {name} {value} not found")
@staticmethod
def get_option_key_by_name_and_value(name, value):
optionsets = OptionSet.list()
for optionset in optionsets:
if optionset["name"] == name:
options = optionset.get("options", [])
for option in options:
if option["value"] == value:
return option["key"]
raise Exception(f"optionset {name} {value} not found")
class Artifact(object):
WORKSHEET_ID = "artifact"
def __init__(self):
pass
@staticmethod
def list(filter: dict):
result = WorksheetRow.list(Artifact.WORKSHEET_ID, filter)
return result
@staticmethod
def update(rowid, fields: list):
row_id = WorksheetRow.update(Artifact.WORKSHEET_ID, rowid, fields)
return row_id
@staticmethod
def create(fields: list):
row_id = WorksheetRow.create(Artifact.WORKSHEET_ID, fields)
return row_id
@staticmethod
def update_by_type_and_value(data: dict):
# 第一层必须是group
filter = {
"type": "group",
"logic": "AND",
"children": [
{
"type": "condition",
"field": "type",
"operator": "eq",
"value": data["type"]
},
{
"type": "condition",
"field": "value",
"operator": "eq",
"value": data["value"]
}
]
}
rows = Artifact.list(filter)
if rows:
for row in rows:
rowid = row['rowId']
fields = [
{"id": "enrichment", "value": data["enrichment"]},
]
rowid_updated = Artifact.update(rowid, fields)
return rowid_updated
else:
fields = [
{"id": "type", "value": data["type"], "type": 2},
{"id": "value", "value": data["value"]},
{"id": "enrichment", "value": data["enrichment"]},
]
rowid_created = Artifact.create(fields)
return rowid_created
class Alert(object):
WORKSHEET_ID = "alert"
def __init__(self):
pass
@staticmethod
def create(fields: list):
row_id = WorksheetRow.create(Alert.WORKSHEET_ID, fields)
return row_id
class Case(object):
WORKSHEET_ID = "case"
def __init__(self):
pass
@staticmethod
def create(fields: list):
row_id = WorksheetRow.create(Case.WORKSHEET_ID, fields)
return row_id
@staticmethod
def update(row_id, fields: list):
row_id = WorksheetRow.update(Case.WORKSHEET_ID, row_id, fields)
return row_id
@staticmethod
def get_by_deduplication_key(deduplication_key: str):
filter = {
"type": "group",
"logic": "AND",
"children": [
{
"type": "condition",
"field": "deduplication_key",
"operator": "eq",
"value": deduplication_key
},
]
}
rows = WorksheetRow.list(Case.WORKSHEET_ID, filter)
if rows:
if len(rows) > 1:
raise Exception(f"found multiple cases with deduplication_key {deduplication_key}")
return rows[0]
else:
return None
class OptionAPI(object):
def __init__(self):
pass
@staticmethod
def to_value_list(options: list):
value_list = []
for option in options:
value_list.append(option.get("value"))
return value_list
+62
View File
@@ -0,0 +1,62 @@
import datetime
import time
def timestamp_to_string(timestamp, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
"""
current_timestamp = 1672531200 # 对应 2023-01-01 00:00:00
# 转换为默认格式的时间字符串
time_string_default = timestamp_to_string(current_timestamp)
print(f"默认格式: {time_string_default}")
# 转换为带毫秒的格式
time_string_with_ms = timestamp_to_string(current_timestamp, "%Y-%m-%d %H:%M:%S.%f")
print(f"带毫秒格式: {time_string_with_ms}")
# 转换为只包含日期和时区的格式
time_string_custom = timestamp_to_string(current_timestamp, "%Y/%m/%d %Z")
print(f"自定义格式: {time_string_custom}")
"""
dt_object = datetime.datetime.fromtimestamp(timestamp)
return dt_object.strftime(format_str)
def string_to_timestamp(time_string: str, format_str: str = "%Y-%m-%dT%H:%M:%S") -> int:
"""
time_string = "2023-01-01 00:00:00"
timestamp_result = string_to_timestamp(time_string)
print(f"时间戳结果: {timestamp_result}")
time_string_custom = "2023/12/25 10:30:00"
timestamp_custom = string_to_timestamp(time_string_custom, "%Y/%m/%d %H:%M:%S")
time_string = "2025-09-18T14:51:30Z"
timestamp_result = string_to_timestamp(time_string, "%Y-%m-%dT%H:%M:%SZ")
"""
dt_object = datetime.datetime.strptime(time_string, format_str)
return int(dt_object.timestamp())
def get_current_timestamp() -> int:
"""
current_ts = get_current_timestamp()
print(f"当前时间戳: {current_ts}")
"""
return int(time.time())
def get_current_time_string(format_str: str = "%Y-%m-%dT%H:%M:%SZ") -> str:
"""
# 示例
# 默认格式
current_time_str = get_current_time_string()
print(f"当前时间字符串(默认格式): {current_time_str}")
# 自定义格式:年-月-日
current_date_str = get_current_time_string("%Y-%m-%d")
print(f"当前时间字符串(自定义格式): {current_date_str}")
"""
return datetime.datetime.now().strftime(format_str)
+75
View File
@@ -0,0 +1,75 @@
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional, Union
class RuleDefinition:
"""
优化版规则定义。
指纹生成逻辑可以处理一个可选的外部时间戳。
"""
def __init__(self,
rule_id: str,
rule_name: str,
deduplication_fields: List[str],
case_title_template: str,
deduplication_window: str,
source: str,
):
self.rule_id = rule_id
self.rule_name = rule_name
self.deduplication_fields = deduplication_fields
self.case_title_template = case_title_template
self.source = source
valid_windows = ['10m', '30m', '1h', '8h', '12h', '24h']
if deduplication_window not in valid_windows:
raise ValueError(f"'{deduplication_window}' 不是一个有效的时间窗口选项。请从 {valid_windows} 中选择。")
self.deduplication_window = deduplication_window
@staticmethod
def _get_time_bucket(dt_object: datetime, window: str) -> datetime:
if window.endswith('m'):
minutes = int(window[:-1])
new_minute = (dt_object.minute // minutes) * minutes
return dt_object.replace(minute=new_minute, second=0, microsecond=0)
elif window.endswith('h'):
hours = int(window[:-1])
if hours == 24:
return dt_object.replace(hour=0, minute=0, second=0, microsecond=0)
else:
new_hour = (dt_object.hour // hours) * hours
return dt_object.replace(hour=new_hour, minute=0, second=0, microsecond=0)
return dt_object
def generate_deduplication_key(self,
artifacts: List[Dict[str, Any]],
timestamp: Optional[Union[int, float]] = None) -> str:
"""
生成包含“时间桶”的去重指纹。
:param artifacts: 事件中的凭据列表
:param timestamp: (可选) 事件的UTC Unix时间戳 (整数或浮点数)。如果为None,则使用当前系统时间。
:return: 包含时间桶的去重指纹
"""
if timestamp is not None:
processing_dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
else:
processing_dt = datetime.now(timezone.utc)
time_bucket_dt = self._get_time_bucket(processing_dt, self.deduplication_window)
time_bucket_str = time_bucket_dt.strftime('%Y-%m-%dT%H:%M:%S')
key_parts = [self.rule_id, time_bucket_str]
artifacts_map = {art['type']: art['value'] for art in artifacts}
for field in sorted(self.deduplication_fields):
key_parts.append(artifacts_map.get(field, 'N/A'))
return "-".join(key_parts)
def generate_case_title(self, artifacts: List[Dict[str, Any]]) -> str:
template_values = {"rule_name": self.rule_name}
for art in artifacts:
template_values[art['type']] = art['value']
return self.case_title_template.format_map(template_values)