Skip to content

Commit ce6450c

Browse files
committed
docs(integration): add integration guide and working examples
Add comprehensive documentation for using kspec as a Discovery Tool and Check Engine, covering both CLI and Go API usage patterns. - Add docs/integration.md with Discovery, Check Engine, Scoring, Reports, CI/CD, and Provider Registration sections - Add _examples/ with working Go examples (discovery, check-engine) - Add CI/CD templates (GitHub Actions, GitLab CI, shell wrapper) - Add minimal policy files for TLS and GitHub providers - Update docs/README.md with Integration section
1 parent 7896c36 commit ce6450c

15 files changed

Lines changed: 1349 additions & 0 deletions

File tree

_examples/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# kspec Examples
2+
3+
Working examples that demonstrate how to use kspec as a **Discovery Tool** and as a **Check Engine**, both via CLI and Go API.
4+
5+
## Quick Start
6+
7+
All Go examples live in the main module — no separate `go.mod` needed:
8+
9+
```bash
10+
# From the repository root:
11+
go run ./_examples/discovery/basic/ example.com
12+
```
13+
14+
## Examples
15+
16+
### Discovery (Go API)
17+
18+
| Example | Description | Run |
19+
|---------|-------------|-----|
20+
| [discovery/basic](discovery/basic/) | `discovery.Discover()` with table output | `go run ./_examples/discovery/basic/ example.com` |
21+
| [discovery/inventory](discovery/inventory/) | `discovery.QuickInventory()` → JSON | `go run ./_examples/discovery/inventory/ example.com` |
22+
| [discovery/graph](discovery/graph/) | `discovery.DiscoverWithInstances()` + graph export | `go run ./_examples/discovery/graph/ example.com` |
23+
24+
### Check Engine (Go API)
25+
26+
| Example | Description | Run |
27+
|---------|-------------|-----|
28+
| [check-engine/basic](check-engine/basic/) | End-to-end scan with `scanner.NewScanner()` | `go run ./_examples/check-engine/basic/ example.com` |
29+
| [check-engine/advanced](check-engine/advanced/) | Events, scoring, JSON/CSV report export | `go run ./_examples/check-engine/advanced/ example.com` |
30+
31+
### CI/CD
32+
33+
| File | Description |
34+
|------|-------------|
35+
| [ci-cd/github-actions.yml](ci-cd/github-actions.yml) | GitHub Actions workflow |
36+
| [ci-cd/gitlab-ci.yml](ci-cd/gitlab-ci.yml) | GitLab CI pipeline |
37+
| [ci-cd/scan.sh](ci-cd/scan.sh) | Shell script wrapper |
38+
39+
### Policies
40+
41+
| File | Description |
42+
|------|-------------|
43+
| [policies/minimal-tls.yaml](policies/minimal-tls.yaml) | Minimal TLS policy (3 checks) |
44+
| [policies/minimal-github.yaml](policies/minimal-github.yaml) | Minimal GitHub org policy (3 checks) |
45+
46+
## Provider Registration
47+
48+
Every Go example that uses kspec must import the provider registration package:
49+
50+
```go
51+
import (
52+
_ "github.com/kopexa-grc/kspec/provider/all"
53+
)
54+
```
55+
56+
This blank import triggers `init()` functions that register all providers (network, GitHub, AWS, Azure, etc.). Without it, `discovery.Discover()` and `scanner.NewScanner()` will fail because no providers are available.
57+
58+
If you only need a specific provider, import it directly:
59+
60+
```go
61+
import (
62+
_ "github.com/kopexa-grc/kspec/provider/network"
63+
)
64+
```
65+
66+
## Using Examples as Standalone Projects
67+
68+
To use an example outside the kspec repository, create your own module:
69+
70+
```bash
71+
mkdir my-scanner && cd my-scanner
72+
go mod init my-scanner
73+
go get github.com/kopexa-grc/kspec@latest
74+
```
75+
76+
Then copy the example `main.go` and run it:
77+
78+
```bash
79+
go run . example.com
80+
```
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) Kopexa GmbH
2+
// SPDX-License-Identifier: Elastic-2.0
3+
4+
// Example: Advanced scanner with events, scoring, and report export.
5+
//
6+
// This example demonstrates:
7+
// - Listening to scan events for progress tracking
8+
// - Using the scoring API (NewGraphScorer + ScoreTree)
9+
// - Exporting reports to JSON and CSV
10+
//
11+
// Usage:
12+
//
13+
// go run ./_examples/check-engine/advanced/ example.com
14+
package main
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"runtime"
22+
23+
"github.com/kopexa-grc/kspec/core"
24+
"github.com/kopexa-grc/kspec/policy"
25+
"github.com/kopexa-grc/kspec/policy/scoring"
26+
"github.com/kopexa-grc/kspec/provider/scanner"
27+
"github.com/kopexa-grc/kspec/report"
28+
29+
// IMPORTANT: Register all providers via blank import.
30+
_ "github.com/kopexa-grc/kspec/provider/all"
31+
)
32+
33+
func main() {
34+
if len(os.Args) < 2 {
35+
fmt.Fprintf(os.Stderr, "Usage: %s <domain>\n", os.Args[0])
36+
os.Exit(1)
37+
}
38+
39+
target := os.Args[1]
40+
ctx := context.Background()
41+
42+
// --- Load policy ---
43+
policyPath := policyFile()
44+
45+
policies, err := policy.Load(policyPath, "")
46+
if err != nil {
47+
fmt.Fprintf(os.Stderr, "Failed to load policy: %v\n", err)
48+
os.Exit(1)
49+
}
50+
51+
policies = policy.FilterByProvider(policies, "network", "network")
52+
53+
// --- Create scanner ---
54+
s := scanner.NewScanner(scanner.ScanConfig{
55+
ProviderName: "network",
56+
ProviderConfig: map[string]string{"target": target},
57+
Asset: core.Asset{
58+
Type: "host",
59+
Name: target,
60+
Config: map[string]string{"target": target},
61+
},
62+
Policies: policies,
63+
})
64+
65+
// --- Listen to events ---
66+
s.OnEvent(func(event scanner.ScanEvent) {
67+
switch event.Type {
68+
case scanner.EventDiscoveryStarted:
69+
fmt.Fprintln(os.Stderr, "[event] Discovery started")
70+
case scanner.EventDiscoveryComplete:
71+
fmt.Fprintln(os.Stderr, "[event] Discovery complete")
72+
case scanner.EventScanStarted:
73+
fmt.Fprintln(os.Stderr, "[event] Scan started")
74+
case scanner.EventResourceScanning:
75+
fmt.Fprintf(os.Stderr, "[event] Scanning: %s\n", event.ResourceType)
76+
case scanner.EventResourceComplete:
77+
fmt.Fprintf(os.Stderr, "[event] Complete: %s\n", event.ResourceType)
78+
case scanner.EventScanComplete:
79+
fmt.Fprintln(os.Stderr, "[event] Scan complete")
80+
case scanner.EventError:
81+
fmt.Fprintf(os.Stderr, "[event] Error: %v\n", event.Error)
82+
}
83+
})
84+
85+
// --- Initialize and run ---
86+
if err := s.Initialize(ctx); err != nil {
87+
fmt.Fprintf(os.Stderr, "Initialize failed: %v\n", err)
88+
os.Exit(1)
89+
}
90+
91+
result := s.Run(ctx)
92+
for _, scanErr := range result.Errors {
93+
fmt.Fprintf(os.Stderr, "Error: %v\n", scanErr)
94+
}
95+
96+
if result.Tree == nil || result.Tree.Root == nil {
97+
fmt.Fprintln(os.Stderr, "No results.")
98+
os.Exit(1)
99+
}
100+
101+
// --- Scoring ---
102+
scorer := scoring.NewGraphScorer(scoring.SystemBanded)
103+
score := scorer.ScoreTree(result.Tree)
104+
findings := scorer.GetRootFindings(result.Tree)
105+
106+
fmt.Println("\n=== Score Summary ===")
107+
fmt.Printf("Score: %d/100\n", score.Value)
108+
fmt.Printf("Grade: %s\n", score.Grade)
109+
fmt.Printf("Risk Level: %s\n", score.RiskLevel)
110+
fmt.Printf("Findings: %d critical, %d high, %d medium, %d low\n",
111+
findings.Critical, findings.High, findings.Medium, findings.Low)
112+
113+
// --- Export reports ---
114+
rep := report.FromResourceTreeWithScoring(result.Tree, "network", scoring.SystemBanded)
115+
116+
// JSON report
117+
jsonPath := "report.json"
118+
if err := report.NewJSONExporter(true).Export(rep, jsonPath); err != nil {
119+
fmt.Fprintf(os.Stderr, "JSON export failed: %v\n", err)
120+
} else {
121+
fmt.Printf("\nJSON report: %s\n", jsonPath)
122+
}
123+
124+
// CSV report
125+
csvPath := "report.csv"
126+
if err := report.NewCSVExporter().Export(rep, csvPath); err != nil {
127+
fmt.Fprintf(os.Stderr, "CSV export failed: %v\n", err)
128+
} else {
129+
fmt.Printf("CSV report: %s\n", csvPath)
130+
}
131+
}
132+
133+
// policyFile returns the path to the bundled policy.yaml next to this example.
134+
func policyFile() string {
135+
_, filename, _, _ := runtime.Caller(0)
136+
return filepath.Join(filepath.Dir(filename), "policy.yaml")
137+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
apiVersion: kspec/v1
2+
kind: Policy
3+
metadata:
4+
name: advanced-scan-example
5+
version: "1.0.0"
6+
7+
scoring_system: banded
8+
9+
require:
10+
- provider: network
11+
12+
groups:
13+
- title: TLS Security
14+
checks:
15+
- uid: tls-modern-version
16+
- uid: tls-no-legacy
17+
- uid: tls-cert-trusted
18+
- uid: tls-cert-not-expiring
19+
20+
- title: HTTP Security Headers
21+
checks:
22+
- uid: http-hsts
23+
24+
queries:
25+
- uid: tls-modern-version
26+
title: TLS 1.2 or higher is supported
27+
resource: tls
28+
severity: high
29+
query: resource.versions.exists(v, v == "tls1.2") || resource.versions.exists(v, v == "tls1.3")
30+
remediation: Enable TLS 1.2 or 1.3 on your server.
31+
32+
- uid: tls-no-legacy
33+
title: Legacy TLS versions (1.0, 1.1) are disabled
34+
resource: tls
35+
severity: medium
36+
query: "!resource.versions.exists(v, v == \"tls1.0\") && !resource.versions.exists(v, v == \"tls1.1\")"
37+
remediation: Disable TLS 1.0 and TLS 1.1 in your server configuration.
38+
39+
- uid: tls-cert-trusted
40+
title: Certificate chain is trusted
41+
resource: tls
42+
severity: critical
43+
query: resource.certificates.size() > 0 && resource.certificates[0].isVerified == true
44+
45+
- uid: tls-cert-not-expiring
46+
title: Certificate expires in more than 30 days
47+
resource: tls
48+
severity: medium
49+
query: resource.certificates.size() > 0 && resource.certificates[0].expiresIn.days > 30
50+
51+
- uid: http-hsts
52+
title: HTTP Strict-Transport-Security header is present
53+
resource: http
54+
severity: medium
55+
query: has(resource.headers) && "Strict-Transport-Security" in resource.headers
56+
remediation: |
57+
Add the `Strict-Transport-Security` response header to enforce HTTPS.
58+
Example: `Strict-Transport-Security: max-age=31536000; includeSubDomains`
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Kopexa GmbH
2+
// SPDX-License-Identifier: Elastic-2.0
3+
4+
// Example: Basic scanner API usage (end-to-end scan).
5+
//
6+
// This example loads a policy, creates a scanner, and runs a full
7+
// scan against a network host. Results are printed as a summary.
8+
//
9+
// Usage:
10+
//
11+
// go run ./_examples/check-engine/basic/ example.com
12+
package main
13+
14+
import (
15+
"context"
16+
"fmt"
17+
"os"
18+
"path/filepath"
19+
"runtime"
20+
21+
"github.com/kopexa-grc/kspec/core"
22+
"github.com/kopexa-grc/kspec/policy"
23+
"github.com/kopexa-grc/kspec/policy/scoring"
24+
"github.com/kopexa-grc/kspec/provider/scanner"
25+
"github.com/kopexa-grc/kspec/report"
26+
27+
// IMPORTANT: Register all providers via blank import.
28+
_ "github.com/kopexa-grc/kspec/provider/all"
29+
)
30+
31+
func main() {
32+
if len(os.Args) < 2 {
33+
fmt.Fprintf(os.Stderr, "Usage: %s <domain>\n", os.Args[0])
34+
os.Exit(1)
35+
}
36+
37+
target := os.Args[1]
38+
ctx := context.Background()
39+
40+
// --- Step 1: Load policy ---
41+
policyPath := policyFile()
42+
43+
policies, err := policy.Load(policyPath, "")
44+
if err != nil {
45+
fmt.Fprintf(os.Stderr, "Failed to load policy: %v\n", err)
46+
os.Exit(1)
47+
}
48+
49+
// Filter policies for the network provider.
50+
policies = policy.FilterByProvider(policies, "network", "network")
51+
if len(policies) == 0 {
52+
fmt.Fprintf(os.Stderr, "No policies found for network provider\n")
53+
os.Exit(1)
54+
}
55+
56+
// --- Step 2: Create scanner ---
57+
s := scanner.NewScanner(scanner.ScanConfig{
58+
ProviderName: "network",
59+
ProviderConfig: map[string]string{"target": target},
60+
Asset: core.Asset{
61+
Type: "host",
62+
Name: target,
63+
Config: map[string]string{"target": target},
64+
},
65+
Policies: policies,
66+
})
67+
68+
// --- Step 3: Initialize provider ---
69+
if err := s.Initialize(ctx); err != nil {
70+
fmt.Fprintf(os.Stderr, "Failed to initialize: %v\n", err)
71+
os.Exit(1)
72+
}
73+
74+
// --- Step 4: Run scan ---
75+
result := s.Run(ctx)
76+
77+
// Print errors if any.
78+
for _, scanErr := range result.Errors {
79+
fmt.Fprintf(os.Stderr, "Error: %v\n", scanErr)
80+
}
81+
82+
if result.Tree == nil || result.Tree.Root == nil {
83+
fmt.Fprintln(os.Stderr, "No results.")
84+
os.Exit(1)
85+
}
86+
87+
// --- Step 5: Score and report ---
88+
rep := report.FromResourceTreeWithScoring(result.Tree, "network", scoring.SystemBanded)
89+
90+
fmt.Printf("Target: %s\n", rep.Metadata.ScanTarget)
91+
fmt.Printf("Score: %d/100 (Grade: %s)\n", rep.Metadata.Score, rep.Metadata.Grade)
92+
fmt.Printf("Risk: %s\n", rep.Metadata.RiskLevel)
93+
fmt.Printf("Checks: %d total, %d passed, %d failed, %d skipped\n",
94+
rep.Metadata.TotalChecks,
95+
rep.Metadata.PassedChecks,
96+
rep.Metadata.FailedChecks,
97+
rep.Metadata.SkippedCheck,
98+
)
99+
100+
// Exit with non-zero code if there are failed checks.
101+
if rep.Metadata.FailedChecks > 0 {
102+
os.Exit(2)
103+
}
104+
}
105+
106+
// policyFile returns the path to the bundled policy.yaml next to this example.
107+
func policyFile() string {
108+
_, filename, _, _ := runtime.Caller(0)
109+
return filepath.Join(filepath.Dir(filename), "policy.yaml")
110+
}

0 commit comments

Comments
 (0)