Marketplace

Xây dựng Content trên Marketplace

Tài liệu này hướng dẫn Creator upload nội dung (content pack) lên Marketplace của hệ thống SecOps. Nội dung package bao gồm cả Public và Private.

ccsp-client là công cụ CLI chính thức để quản lý các security package trên SecOps Marketplace.

Hỗ trợ: Linux · macOS · Windows


Hướng dẫn upload nội dung lên Git

Bước 1: Tải CLI

Tải CLI theo đường link sau: Coming soon

Bước 2: Xây dựng Content trên CLI

1. Cấu hình xác thực

  • Đầu tiên, người dùng cần cấu hình IAM service account credentials:
ccsp-client add-access-key \
  --id     <ACCESS_KEY_ID> \
  --secret <ACCESS_KEY_SECRET> \
  --project <PROJECT_ID>

Trong đó:

  • <ACCESS_KEY_ID> và <ACCESS_KEY_SECRET> được tạo từ tính năng "Access Key" trong Platform Management.
  • Sau khi đã cấu hình xác thực, credentials được lưu tại:
OSĐường dẫn
Linux/ macOS~/.csop-client/config
Windows%USERPROFILE%.csop-client\config

2. Khởi tạo Package

  • Tạo cấu trúc thư mục và file package.json mẫu cho một package mới.
# Cú pháp: 
ccsp-client init <package-name>

# Ví dụ:
ccsp-client init my-security-package
  • Output:
# Trả về dòng lệnh
Package 'my-security-package' initialized successfully!

# Cấu trúc package:

<package-name>/
├── package.json          # Metadata bắt buộc
├── README.md             # Tài liệu (khuyến nghị)
├── connectors/           # Cấu hình connector (.json)
├── dashboards/           # Định nghĩa dashboard (.json)
├── parsers/              # Log parser (.yaml / .yml)
└── rules/
    ├── streaming/        # Streaming detection rules (.yaml / .yml)
    └── correlation/      # Correlation rules (.yaml / .yml)

Trong đó, chi tiết package JSON bao gồm:

TrườngKiểu dữ liệuBắt buộcMô tả
authorstringTên tác giả / username
categoryarrayDanh sách category. Vd: Network, Firewall
capabilityarrayDanh sách capability. Version hiện tại support 2 loại : rule, parser
descriptionstringMô tả ngắn gọn
namestringTên định danh package. Vd: Fortigate Firewall
publicbooleanBao gồm: Public / Private (giá trị mặc định)
vendorstringCông ty / tổ chức sở hữu
versionstringSemantic version MAJOR.MINOR.PATCH

Bước 2: Xây dựng content pack

Hiện tại, hệ thống chỉ đang support xây dựng content cho: Parser và Rule (Streaming)

Bước 3: Kiểm thử nội dung

1. Validate cấu trúc

# Cú pháp:
ccsp-client validate <package-path>

# Ví dụ:

ccsp-client validate my-security-package
ccsp-client validate ./path/to/package
ccsp-client validate .   # validate thư mục hiện tại
# Output thành công:
Validating package...
Package validation successful!
Your package is ready for testing and publishing.

# Output lỗi (Ví dụ):
Validating package...
Validation failed: package.json: missing required field "vendor"
Chú ý: Các điều kiện validation:
  • File package.json phải tồn tại và hợp lệ JSON
  • Các trường bắt buộc: name, description, version, vendor, author, category
  • Ít nhất một thư mục con (parsers/, rules/, dashboards/, connectors/) phải có nội dung
  • Tất cả file .yaml/.yml phải có cú pháp YAML hợp lệ
  • Tất cả file .json phải có cú pháp JSON hợp lệ

2. Kiểm thử ở Sandbox

Gửi parsers và rules đến backend sandbox để kiểm thử. Yêu cầu credentials.

# Cú pháp:
ccsp-client test <package-path>

# Ví dụ:
ccsp-client test my-security-package
ccsp-client test ./path/to/package
# Output thành công:
Testing package files...
 Test passed: parsers/apache-parser.yaml
 Test passed: rules/streaming/suspicious-login.json
 Test success: 2 file(s) tested successfully

# Ví dụ output lỗi:
Credentials not found. Please run 'ccsp-client add-access-key' first
Lưu ý: Chạy validate trước test để phát hiện lỗi cú pháp mà không tốn API call.
Rule testing: Khi test rule (.yaml/.yml trong rules/), backend không cần sample data, chỉ validate cú pháp và cấu trúc Sigma YAML. Kết quả trả về validation_status cho biết rule hợp lệ hay không.

Bước 4: Xuất bản nội dung lên Marketplace

Lưu ý:
  • Đối với Public Package, sau khi nội dung được publish lên hệ thống Git, các gói này sẽ ở trạng thái chờ (Pending). Quản trị viên cần thực hiện phê duyệt (Approve) để nội dung chính thức hiển thị trên Marketplace.
  • Đối với Private Package, nội dung package sẽ hiển thị ngay trên Marketplace sau khi creator publish lên mà không cần qua bước phê duyệt của Admin

Validate, nén và upload package lên SecOps Marketplace. Yêu cầu credentials.

#Cú pháp:
ccsp-client publish <package-path>
ccsp-client publish <package-path>:<version>
# Ví dụ:
ccsp-client publish my-security-package
# Chỉ định version cụ thể
ccsp-client publish my-security-package:1.0.2
ccsp-client publish ./path/to/package:1.0.5
Quick edit: Có thể sửa nhanh version bằng cách điền sau mà không cần điền vào package.json
# Output thành công:
Publishing package...
 Package validation passed
 Created archive: my-security-package.zip
 Package published successfully
Your package is now pending review by the marketplace team

Bước 5: Quản trị viên Approve Content Pack (chỉ áp dụng với Public Package)

Admin thực hiện approve content pack trên GIT. Sau khi được approve, nội dung của gói sẽ được đẩy lên Marketplace.


Hướng dẫn dành cho newbie: từ cài đặt credentials đến publish thành công package Fortigate Firewall lên SecOps Marketplace.

Mục lục

  1. Chuẩn bị
  2. Bước 1 — Cấu hình credentials
  3. Bước 2 — Khởi tạo package
  4. Bước 3 — Chỉnh sửa package.json
  5. Bước 4 — Tạo Parser
  6. Bước 5 — Tạo Rule Detection
  7. Bước 6 — Validate
  8. Bước 7 — Test
  9. Bước 8 — Publish
  10. Tổng kết cấu trúc thư mục

1. Chuẩn bị

Đảm bảo bạn đã có:

  • File binary ccsp-client (đặt trong thư mục làm việc hoặc trong $PATH)
  • IAM credentials: ACCESS_KEY_IDACCESS_KEY_SECRET do admin cấp
  • PROJECT_ID của project bạn thuộc về

Kiểm tra version:

./ccsp-client --version

Bước 1 — Cấu hình credentials

Chạy lệnh sau một lần duy nhất. Credentials sẽ được lưu vào ~/.ccsp-client/config.

./ccsp-client add-access-key \
  --id     YOUR_ACCESS_KEY_ID \
  --secret YOUR_ACCESS_KEY_SECRET \
  --project YOUR_PROJECT_ID

🔒 File config tự động được tạo với quyền 0600 — chỉ bạn mới đọc được.


Bước 2 — Khởi tạo package

./ccsp-client init "Fortigate Firewall"

Lệnh tạo thư mục Fortigate Firewall/ với cấu trúc sau:

Fortigate Firewall/
├── connectors/
├── dashboards/
├── parsers/
├── rules/
│   ├── streaming/
│   └── correlation/
└── package.json

Di chuyển vào thư mục vừa tạo:

cd "Fortigate Firewall"

Bước 3 — Chỉnh sửa package.json

Mở package.json và cập nhật các trường. Sau khi init, file trông như sau:

{
  "name": "Fortigate Firewall",
  "description": "",
  "version": "1.0.0",
  "vendor": "",
  "author": "",
  "category": [],
  "capability": []
}

Sửa thành:

{
  "name": "Fortigate Firewall",
  "description": "Parser and detection rules for Fortinet FortiGate firewall logs",
  "version": "1.0.0",
  "vendor": "Fortinet",
  "author": "your-username",
  "category": ["Network", "Firewall"],
  "capability": ["parser", "rule"],
  "public": true
}

⚠️ categorycapability thường bị rỗng sau khi init — bắt buộc phải điền tay.


Bước 4 — Tạo Parser

Tạo file parser tại parsers/fortigate-firewall-parser.yaml:

touch parsers/fortigate-firewall-parser.yaml

Dán nội dung sau vào file:

#regex


#conditional
log_message = ""
if (msg, err = to_string(.message); err == null) { 
  log_message = msg
}

event_timestamp = ""
log_date = ""
log_time = ""
event_module = "fortigate"
log_id = ""
log_type = ""
log_subtype = ""
event_category = ""
event_action = ""
event_outcome = ""
log_level = "info"

source_ip = ""
source_port = ""
source_interface = ""
destination_ip = ""
destination_port = ""
destination_interface = ""
network_protocol = ""
network_transport = ""

user_name = ""
group_name = ""

policy_id = ""
policy_uuid = ""
session_id = ""
action = ""

threat_name = ""
threat_category = ""
attack_name = ""
attack_id = ""

url_full = ""
url_domain = ""
file_name = ""

server_address = ""
client_address = ""

if (date_m, err = parse_regex(log_message, r'date=(?P<date>\d{4}-\d{2}-\d{2})'); err == null) {
  log_date = to_string(date_m.date)
}

if (time_m, err = parse_regex(log_message, r'time=(?P<time>\d{2}:\d{2}:\d{2})'); err == null) {
  log_time = to_string(time_m.time)
}

if (logid_m, err = parse_regex(log_message, r'logid="(?P<id>[^"]+)"'); err == null) {
  log_id = to_string(logid_m.id)
}

if (type_m, err = parse_regex(log_message, r'\stype="(?P<type>[^"]+)"'); err == null) {
  log_type = to_string(type_m.type)
}

if (subtype_m, err = parse_regex(log_message, r'subtype="(?P<subtype>[^"]+)"'); err == null) {
  log_subtype = to_string(subtype_m.subtype)
}

if (level_m, err = parse_regex(log_message, r'level="(?P<level>[^"]+)"'); err == null) {
  log_level = to_string(level_m.level)
}

if (srcip_m, err = parse_regex(log_message, r'srcip=(?P<ip>[0-9.]+)'); err == null) {
  source_ip = to_string(srcip_m.ip)
}

if (srcport_m, err = parse_regex(log_message, r'srcport=(?P<port>\d+)'); err == null) {
  source_port = to_string(srcport_m.port)
}

if (srcintf_m, err = parse_regex(log_message, r'srcintf="(?P<intf>[^"]+)"'); err == null) {
  source_interface = to_string(srcintf_m.intf)
}

if (dstip_m, err = parse_regex(log_message, r'dstip=(?P<ip>[0-9.]+)'); err == null) {
  destination_ip = to_string(dstip_m.ip)
}

if (dstport_m, err = parse_regex(log_message, r'dstport=(?P<port>\d+)'); err == null) {
  destination_port = to_string(dstport_m.port)
}

if (dstintf_m, err = parse_regex(log_message, r'dstintf="(?P<intf>[^"]+)"'); err == null) {
  destination_interface = to_string(dstintf_m.intf)
}

if (proto_m, err = parse_regex(log_message, r'\sproto=(?P<proto>\d+)'); err == null) {
  proto_num = to_string(proto_m.proto)
  if proto_num == "6" { network_transport = "tcp" }
  if proto_num == "17" { network_transport = "udp" }
  if proto_num == "1" { network_transport = "icmp" }
}

if (service_m, err = parse_regex(log_message, r'service="(?P<service>[^"]+)"'); err == null) {
  network_protocol = downcase(to_string(service_m.service))
}

if (policyid_m, err = parse_regex(log_message, r'policyid=(?P<id>\d+)'); err == null) {
  policy_id = to_string(policyid_m.id)
}

if (poluuid_m, err = parse_regex(log_message, r'poluuid="(?P<uuid>[^"]+)"'); err == null) {
  policy_uuid = to_string(poluuid_m.uuid)
}

if (sessionid_m, err = parse_regex(log_message, r'sessionid=(?P<id>\d+)'); err == null) {
  session_id = to_string(sessionid_m.id)
}

if (action_m, err = parse_regex(log_message, r'action="(?P<action>[^"]+)"'); err == null) {
  action = to_string(action_m.action)
}

if (user_m, err = parse_regex(log_message, r'\suser="(?P<user>[^"]+)"'); err == null) {
  user_name = to_string(user_m.user)
}

if (group_m, err = parse_regex(log_message, r'group="(?P<group>[^"]+)"'); err == null) {
  group_name = to_string(group_m.group)
}

if (hostname_m, err = parse_regex(log_message, r'hostname="(?P<host>[^"]+)"'); err == null) {
  url_domain = to_string(hostname_m.host)
  server_address = to_string(hostname_m.host)
}

if (url_m, err = parse_regex(log_message, r'\surl="(?P<url>[^"]+)"'); err == null) {
  url_full = to_string(url_m.url)
}

if (filename_m, err = parse_regex(log_message, r'filename="(?P<file>[^"]+)"'); err == null) {
  file_name = to_string(filename_m.file)
}

if (attack_m, err = parse_regex(log_message, r'attack="(?P<attack>[^"]+)"'); err == null) {
  attack_name = to_string(attack_m.attack)
  threat_name = attack_name
}

if (attackid_m, err = parse_regex(log_message, r'attackid=(?P<id>\d+)'); err == null) {
  attack_id = to_string(attackid_m.id)
}

if (virus_m, err = parse_regex(log_message, r'virus="(?P<virus>[^"]+)"'); err == null) {
  threat_name = to_string(virus_m.virus)
}

if (catdesc_m, err = parse_regex(log_message, r'catdesc="(?P<cat>[^"]+)"'); err == null) {
  threat_category = to_string(catdesc_m.cat)
}

log_type_str = to_string(log_type)
log_subtype_str = to_string(log_subtype)
action_str = to_string(action)

if log_type_str == "traffic" {
  event_category = "network"
  event_module = "fortigate.traffic"
  if action_str == "accept" { event_action = "allowed"; event_outcome = "success" }
  if action_str == "deny"   { event_action = "denied";  event_outcome = "failure" }
  if action_str == "close"  { event_action = "connection_closed"; event_outcome = "success" }
}

if log_type_str == "utm" {
  if log_subtype_str == "ips" {
    event_category = "intrusion_detection"
    event_module = "fortigate.ips"
    if action_str == "dropped" { event_action = "blocked";  event_outcome = "success" }
    if action_str == "detected" { event_action = "detected"; event_outcome = "success" }
  }
  if log_subtype_str == "virus" {
    event_category = "malware"
    event_module = "fortigate.antivirus"
    if action_str == "blocked"  { event_action = "blocked";  event_outcome = "success" }
    if action_str == "detected" { event_action = "detected"; event_outcome = "success" }
  }
  if log_subtype_str == "webfilter" {
    event_category = "web"
    event_module = "fortigate.webfilter"
    if action_str == "blocked"     { event_action = "blocked"; event_outcome = "success" }
    if action_str == "passthrough" { event_action = "allowed"; event_outcome = "success" }
  }
  if log_subtype_str == "app-ctrl" {
    event_category = "network"
    event_module = "fortigate.application"
    if action_str == "blocked" { event_action = "blocked"; event_outcome = "success" }
    if action_str == "pass"    { event_action = "allowed"; event_outcome = "success" }
  }
  if log_subtype_str == "dns" {
    event_category = "network"
    event_module = "fortigate.dns"
    if action_str == "pass"    { event_action = "allowed"; event_outcome = "success" }
    if action_str == "blocked" { event_action = "blocked"; event_outcome = "success" }
  }
  if log_subtype_str == "anomaly" {
    event_category = "intrusion_detection"
    event_module = "fortigate.anomaly"
    threat_name = attack_name
    if action_str == "clear_session" { event_action = "blocked"; event_outcome = "success" }
  }
}

if log_type_str == "event" {
  if log_subtype_str == "system" {
    event_category = "authentication"
    event_module = "fortigate.event"
    if contains(log_message, "login") {
      if contains(log_message, "success") { event_action = "logon_success"; event_outcome = "success" }
      if contains(log_message, "failed")  { event_action = "logon_failed";  event_outcome = "failure" }
    }
  }
  if log_subtype_str == "user" {
    event_category = "authentication"
    event_module = "fortigate.event"
    if contains(log_message, "authentication") {
      if contains(log_message, "success") { event_action = "logon_success"; event_outcome = "success" }
      if contains(log_message, "failure") { event_action = "logon_failed";  event_outcome = "failure" }
    }
  }
  if log_subtype_str == "vpn" {
    event_category = "network"
    event_module = "fortigate.vpn"
    event_action = "vpn_event"
    event_outcome = "success"
  }
  if log_subtype_str == "router" {
    event_category = "network"
    event_module = "fortigate.router"
    event_action = "routing_event"
    event_outcome = "success"
  }
}

if source_ip != "" { client_address = source_ip }
if event_category == "" { event_category = "network" }
if event_action == ""   { event_action = "info" }
if event_outcome == ""  { event_outcome = "unknown" }

#normalize
timestamp: format_timestamp!(parse_timestamp!(.timestamp, "%Y-%m-%dT%H:%M:%S.%3fZ"), "%Y-%m-%d %H:%M:%S")
event.module: event_module
event.category: event_category
event.action: event_action
event.outcome: event_outcome
log.level: log_level
message: log_message
source.ip: source_ip
source.port: source_port
destination.ip: destination_ip
destination.port: destination_port
network.transport: network_transport
network.protocol: network_protocol
user.name: user_name
group.name: group_name
url.full: url_full
url.domain: url_domain
file.name: file_name
server.address: server_address
client.address: client_address
threat.name: threat_name

Giải thích 3 section:

SectionMục đích
#regexKhai báo regex patterns dùng lại nhiều lần (có thể để trống)
#conditionalVRL pipeline: extract fields từ raw log message
#normalizeMap biến → ECS fields để lưu vào SIEM

Bước 5 — Tạo Rule Detection

Tạo file streaming rule tại rules/streaming/Web-Filter-Detected.yaml:

touch rules/streaming/Web-Filter-Detected.yaml

Dán nội dung sau vào file:

title: "[FortiGate] Web Filter Detected"
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
sigma_status: experimental
description: >
  Detects Web Filter events detected by FortiGate firewall.
  This could indicate a potential security event if the Web Filter
  is blocking access to malicious websites or monitoring user activity.
author: your-username
date: 2026/04/09
logsource:
  product: "fortigate"
  service: "webfilter"
  category: "firewall"
detection:
  selection_1:
    event.category|contains: "utm"
  selection_2:
    event.type|contains: "webfilter"
  condition: selection_1 AND selection_2
falsepositives: []
level: medium

⚠️ Mỗi rule phải có id UUID v4 duy nhất. Tạo UUID bằng lệnh:

uuidgen

Bước 6 — Validate

Validate kiểm tra cú pháp và cấu trúc không cần kết nối API:

cd ..   # quay về thư mục chứa ccsp-client nếu cần
./ccsp-client validate "Fortigate Firewall"

Output thành công:

Validating package...
Package validation successful!
Your package is ready for testing and publishing.

Output lỗi thường gặp và cách sửa:

LỗiNguyên nhânCách sửa
missing required field "vendor"vendor trong package.json bị trốngĐiền giá trị vào package.json
missing required field "capability"capability bị rỗng hoặc thiếuThêm "capability": ["parser", "rule"]
no content foundChưa có file trong parsers/ hoặc rules/Thêm ít nhất 1 file content
invalid YAML syntaxLỗi cú pháp YAMLKiểm tra indentation, dấu ngoặc kép

Bước 7 — Test

Test gửi parser và rule lên backend sandbox để kiểm thử (yêu cầu credentials từ Bước 1):

./ccsp-client test "Fortigate Firewall"

Output thành công:

Testing package files...
✓ Test passed: parsers/fortigate-firewall-parser.yaml
✓ Test passed: rules/streaming/Web-Filter-Detected.yaml

✓ Test success: 2 file(s) tested successfully

Output lỗi thường gặp và cách sửa:

LỗiNguyên nhânCách sửa
Credentials not foundChưa chạy add-access-keyChạy lại Bước 1
no files found to testThư mục parsers/ và rules/ trốngThêm file content
validation_status: failedRule có lỗi cú pháp SigmaKiểm tra detection syntax
Test failed: <tên file>Nội dung parser/rule không hợp lệXem thông báo lỗi chi tiết bên dưới

💡 Tip: Luôn chạy validate trước test để phát hiện lỗi cú pháp ngay local, tránh tốn API call.


Bước 8 — Publish

Sau khi test thành công, publish package lên marketplace:

./ccsp-client publish "Fortigate Firewall":1.0.0

Output thành công:

Publishing package...
✓ Package validation passed
✓ Created archive: Fortigate Firewall.zip
✓ Package published successfully
Your package is now pending review by the marketplace team

Package sẽ ở trạng thái pending review cho đến khi được marketplace team duyệt.

⚠️ Khi muốn cập nhật package, tăng version trong package.json (ví dụ 1.0.01.1.0) rồi publish lại:

./ccsp-client publish "Fortigate Firewall":1.1.0

Tổng kết cấu trúc thư mục

Sau khi hoàn thành, cấu trúc package trông như sau:

Fortigate Firewall/
├── package.json
├── parsers/
│   └── fortigate-firewall-parser.yaml     ← Parser VRL pipeline
└── rules/
    └── streaming/
        └── Web-Filter-Detected.yaml       ← Streaming detection rule

Tóm tắt các lệnh

# 1. Cấu hình credentials (1 lần duy nhất)
./ccsp-client add-access-key --id <ID> --secret <SECRET> --project <PROJECT>

# 2. Khởi tạo package
./ccsp-client init "Fortigate Firewall"
cd "Fortigate Firewall"

# 3. Sửa package.json, thêm parser và rule (xem hướng dẫn bên trên)

# 4. Validate (không cần API)
./ccsp-client validate "Fortigate Firewall"

# 5. Test (cần credentials)
./ccsp-client test "Fortigate Firewall"

# 6. Publish
./ccsp-client publish "Fortigate Firewall":1.0.0