● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / report / report.go
← Back to directory
286 lines · 10.4 KB
Raw
1package report
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "strings"
8 "time"
9
10 "github.com/shehzade/contraband/pkg/common"
11)
12
13// AssessmentSummary aggregates high-level metrics for report headers and summaries.
14type AssessmentSummary struct {
15 EEIScore float64 `json:"eei_score"`
16 Posture PostureLevel `json:"posture"`
17 PostureColor string `json:"posture_color"`
18 Sport float64 `json:"sport"`
19 Sproto float64 `json:"sproto"`
20 Sdlp float64 `json:"sdlp"`
21 TotalProbes int `json:"total_probes"`
22 SuccessCount int `json:"success_count"`
23 BlockedL4 int `json:"blocked_l4_count"`
24 BlockedL7 int `json:"blocked_l7_count"`
25 BlockedDLP int `json:"blocked_dlp_count"`
26 TotalSessions int `json:"total_sessions"`
27 TotalCredentials int `json:"total_credentials"`
28 VerdictCounts map[string]int `json:"verdict_counts"`
29 TotalBytesSent int64 `json:"total_bytes_sent"`
30 TotalBytesRecv int64 `json:"total_bytes_recv"`
31 TotalDurationMS float64 `json:"total_duration_ms"`
32 DurationFormatted string `json:"duration_formatted"`
33 AvgLatencyMS float64 `json:"avg_latency_ms"`
34}
35
36// AssessmentReport is the unified container for all reporting, export, and serving functions.
37type AssessmentReport struct {
38 ID string `json:"id"`
39 ToolName string `json:"tool_name"`
40 Version string `json:"version"`
41 Commit string `json:"commit"`
42 GeneratedAt time.Time `json:"generated_at"`
43 TargetHost string `json:"target_host"`
44 DurationMS float64 `json:"duration_ms"`
45 DurationText string `json:"duration_text"`
46 EEI *EEIReport `json:"eei"`
47 Summary AssessmentSummary `json:"summary"`
48 Probes []common.ProbeResult `json:"probes"`
49 Findings []common.Finding `json:"findings"`
50 Remediation RemediationRuleSet `json:"remediation"`
51 Sessions []*common.Session `json:"sessions,omitempty"`
52 Credentials []common.CapturedCredential `json:"credentials,omitempty"`
53}
54
55// CalculateScoringSummary generates high-level summary metrics from probe results.
56func CalculateScoringSummary(probes []common.ProbeResult) AssessmentSummary {
57 eei := CalculateEEI(probes)
58
59 summary := AssessmentSummary{
60 EEIScore: eei.Score,
61 Posture: eei.Posture,
62 PostureColor: eei.PostureColor,
63 Sport: eei.PortScore.RawScore,
64 Sproto: eei.ProtoScore.RawScore,
65 Sdlp: eei.DLPScore.RawScore,
66 TotalProbes: len(probes),
67 VerdictCounts: make(map[string]int),
68 }
69
70 var totalLatency time.Duration
71 for _, p := range probes {
72 summary.VerdictCounts[string(p.Verdict)]++
73 summary.TotalBytesSent += p.BytesSent
74 summary.TotalBytesRecv += p.BytesRecv
75
76 switch p.Verdict {
77 case common.VerdictSuccess:
78 summary.SuccessCount++
79 case common.VerdictBlockedL4:
80 summary.BlockedL4++
81 case common.VerdictBlockedL7:
82 summary.BlockedL7++
83 case common.VerdictBlockedDLP:
84 summary.BlockedDLP++
85 }
86
87 totalLatency += p.Timestamps.TotalDuration
88 }
89
90 if len(probes) > 0 {
91 avgMs := float64(totalLatency.Microseconds()) / (1000.0 * float64(len(probes)))
92 summary.AvgLatencyMS = avgMs
93 summary.TotalDurationMS = float64(totalLatency.Microseconds()) / 1000.0
94 summary.DurationFormatted = fmt.Sprintf("%.2fms", summary.TotalDurationMS)
95 }
96
97 return summary
98}
99
100// GenerateFindings maps assessment results to standard Contraband findings rules (CTRB-001 through CTRB-004).
101func GenerateFindings(probes []common.ProbeResult, credentials []common.CapturedCredential) []common.Finding {
102 var findings []common.Finding
103 seenPorts := make(map[int]bool)
104 seenProtos := make(map[string]bool)
105
106 // Check credentials for CTRB-004
107 for _, cred := range credentials {
108 findings = append(findings, common.Finding{
109 RuleID: "CTRB-004",
110 Level: "critical",
111 Message: fmt.Sprintf("NetNTLMv2 hash captured for user '%s' domain '%s' via %s", cred.Username, cred.Domain, cred.Protocol),
112 Protocol: cred.Protocol,
113 EEIContribution: 10.0,
114 Remediation: "Disable outbound SMB/WebDAV NTLMSSP authentication and enforce Extended Protection for Authentication (EPA).",
115 })
116 }
117
118 for _, p := range probes {
119 if p.CapturedHash != "" {
120 findings = append(findings, common.Finding{
121 RuleID: "CTRB-004",
122 Level: "critical",
123 Message: fmt.Sprintf("Cleartext NetNTLMv2 authentication hash exposed via %s on port %d", p.Protocol, p.TargetPort),
124 Port: p.TargetPort,
125 Protocol: p.Protocol,
126 EEIContribution: 10.0,
127 Remediation: "Block outbound SMB (TCP 445) and enforce SMB signing / EPA.",
128 })
129 }
130
131 if p.Verdict != common.VerdictSuccess {
132 continue
133 }
134
135 // DLP Interception Failure (CTRB-003)
136 if p.PayloadType != "" {
137 findings = append(findings, common.Finding{
138 RuleID: "CTRB-003",
139 Level: "critical",
140 Message: fmt.Sprintf("DLP failure: %s secret exfiltrated uninspected over %s on port %d", strings.ToUpper(p.PayloadType), p.Protocol, p.TargetPort),
141 Port: p.TargetPort,
142 Protocol: p.Protocol,
143 EEIContribution: 5.0,
144 Remediation: fmt.Sprintf("Configure deep content inspection to inspect and block %s secrets.", p.PayloadType),
145 })
146 continue
147 }
148
149 // Layer 4 Unfiltered Port (CTRB-001)
150 if p.Stage == "sweep" || p.Protocol == "tcp" || p.Protocol == "udp" {
151 if !seenPorts[p.TargetPort] {
152 seenPorts[p.TargetPort] = true
153 level := "warning"
154 switch p.TargetPort {
155 case 22, 23, 135, 139, 445, 1433, 3389:
156 level = "error"
157 }
158 findings = append(findings, common.Finding{
159 RuleID: "CTRB-001",
160 Level: level,
161 Message: fmt.Sprintf("Unfiltered outbound port %d/%s accessible through perimeter firewall", p.TargetPort, p.Protocol),
162 Port: p.TargetPort,
163 Protocol: p.Protocol,
164 EEIContribution: 2.0,
165 Remediation: fmt.Sprintf("Apply egress firewall rule to drop outbound traffic on port %d.", p.TargetPort),
166 })
167 }
168 continue
169 }
170
171 // Layer 7 Smuggling Success (CTRB-002)
172 if !seenProtos[p.Protocol] {
173 seenProtos[p.Protocol] = true
174 findings = append(findings, common.Finding{
175 RuleID: "CTRB-002",
176 Level: "error",
177 Message: fmt.Sprintf("Layer 7 exfiltration channel %s succeeded on port %d", p.Protocol, p.TargetPort),
178 Port: p.TargetPort,
179 Protocol: p.Protocol,
180 EEIContribution: 4.0,
181 Remediation: fmt.Sprintf("Enable deep application inspection and proxy decryption for %s.", p.Protocol),
182 })
183 }
184 }
185
186 return findings
187}
188
189// NewAssessmentReport builds a complete, populated AssessmentReport.
190func NewAssessmentReport(targetHost string, probes []common.ProbeResult, sessions []*common.Session, credentials []common.CapturedCredential, duration time.Duration) *AssessmentReport {
191 id, err := common.GenerateUUIDv4()
192 if err != nil {
193 id = fmt.Sprintf("ctrb-%d", time.Now().UnixNano())
194 }
195
196 eei := CalculateEEI(probes)
197 summary := CalculateScoringSummary(probes)
198 summary.TotalSessions = len(sessions)
199 summary.TotalCredentials = len(credentials)
200
201 durationMs := float64(duration.Microseconds()) / 1000.0
202 summary.TotalDurationMS = durationMs
203 summary.DurationFormatted = fmt.Sprintf("%.2fms", durationMs)
204
205 remediation := GenerateRemediation(probes, targetHost)
206 findings := GenerateFindings(probes, credentials)
207
208 return &AssessmentReport{
209 ID: id,
210 ToolName: "Contraband",
211 Version: common.Version,
212 Commit: common.Commit,
213 GeneratedAt: time.Now().UTC(),
214 TargetHost: targetHost,
215 DurationMS: durationMs,
216 DurationText: fmt.Sprintf("%.2fms", durationMs),
217 EEI: eei,
218 Summary: summary,
219 Probes: probes,
220 Findings: findings,
221 Remediation: *remediation,
222 Sessions: sessions,
223 Credentials: credentials,
224 }
225}
226
227// LoadReport reads and deserializes an assessment report file from disk.
228func LoadReport(filePath string) (*AssessmentReport, error) {
229 data, err := os.ReadFile(filePath)
230 if err != nil {
231 return nil, fmt.Errorf("failed to read report file %s: %w", filePath, err)
232 }
233 return ParseReport(data)
234}
235
236// ParseReport deserializes JSON data into an AssessmentReport, accommodating multiple schema variations.
237func ParseReport(data []byte) (*AssessmentReport, error) {
238 // 1. Full AssessmentReport
239 var rep AssessmentReport
240 if err := json.Unmarshal(data, &rep); err == nil && (len(rep.Probes) > 0 || rep.Summary.TotalProbes > 0 || rep.EEI != nil) {
241 if rep.EEI == nil {
242 rep.EEI = CalculateEEI(rep.Probes)
243 }
244 if rep.Summary.TotalProbes == 0 {
245 rep.Summary = CalculateScoringSummary(rep.Probes)
246 rep.Summary.TotalSessions = len(rep.Sessions)
247 rep.Summary.TotalCredentials = len(rep.Credentials)
248 }
249 if rep.Remediation.PANOSRules == "" && len(rep.Probes) > 0 {
250 rep.Remediation = *GenerateRemediation(rep.Probes, rep.TargetHost)
251 }
252 if len(rep.Findings) == 0 && len(rep.Probes) > 0 {
253 rep.Findings = GenerateFindings(rep.Probes, rep.Credentials)
254 }
255 return &rep, nil
256 }
257
258 // 2. Ledger ExportJSON format
259 var stateExport struct {
260 GeneratedAt time.Time `json:"generated_at"`
261 TotalProbes int `json:"total_probes"`
262 Probes []common.ProbeResult `json:"probes"`
263 TotalSessions int `json:"total_sessions"`
264 Sessions []*common.Session `json:"sessions"`
265 Credentials []common.CapturedCredential `json:"credentials"`
266 }
267 if err := json.Unmarshal(data, &stateExport); err == nil && len(stateExport.Probes) > 0 {
268 targetHost := "127.0.0.1"
269 if len(stateExport.Probes) > 0 && stateExport.Probes[0].TargetHost != "" {
270 targetHost = stateExport.Probes[0].TargetHost
271 }
272 return NewAssessmentReport(targetHost, stateExport.Probes, stateExport.Sessions, stateExport.Credentials, 0), nil
273 }
274
275 // 3. Raw ProbeResult array
276 var rawProbes []common.ProbeResult
277 if err := json.Unmarshal(data, &rawProbes); err == nil && len(rawProbes) > 0 {
278 targetHost := "127.0.0.1"
279 if rawProbes[0].TargetHost != "" {
280 targetHost = rawProbes[0].TargetHost
281 }
282 return NewAssessmentReport(targetHost, rawProbes, nil, nil, 0), nil
283 }
284
285 return nil, fmt.Errorf("unrecognized or invalid assessment report JSON")
286}