[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● 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)
286 lines · 10.4 KB
| 1 | package report |
| 2 | |
| 3 | import ( |
| 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. |
| 14 | type 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. |
| 37 | type 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. |
| 56 | func 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). |
| 101 | func 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. |
| 190 | func 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. |
| 228 | func 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. |
| 237 | func 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 | } |