Skip to main content

rusthound_ce/modules/adcs/
esc8.rs

1//! ESC8 scanner, Web Enrollment HTTP/HTTPS probe + EPA (Channel Binding) detection.
2//!
3//! Detects whether a CA exposes the `/certsrv/certfnsh.asp` endpoint over HTTP
4//! (always vulnerable to NTLM relay) or over HTTPS without Extended Protection for
5//! Authentication (EPA / Channel Binding), which is also vulnerable.
6//!
7//! The EPA check works by sending a minimal NTLM Type 1 (Negotiate) message to the
8//! HTTPS endpoint and parsing the server's NTLM Type 2 (Challenge) response. If the
9//! `MsvAvChannelBindings` AvPair (AvId `0x000A`) is absent from the challenge's
10//! `TargetInfo`, EPA is not enforced and the endpoint is relay-able.
11//!
12//! This approach requires a single HTTP round-trip, no credentials, no full
13//! NTLM handshake, no relay attempted.
14//!
15//! Module path: `src/modules/adcs/esc8.rs`
16//! Required Cargo dependency: `reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls-ring"] }`
17
18use crate::objects::enterpriseca::{WebEnrollmentEndpoint, WebEnrollmentResult};
19use crate::utils::b64::{b64_decode, b64_encode};
20use log::{debug, warn};
21use reqwest::blocking::Client;
22use reqwest::header::{AUTHORIZATION, WWW_AUTHENTICATE};
23use std::time::Duration;
24
25// NTLM AvPair IDs
26
27/// End-of-list marker in NTLM TargetInfo AvPairs.
28const MV_AV_EOL: u16 = 0x0000;
29
30/// `MsvAvChannelBindings`, present with non-zero length when EPA is required.
31const MV_AV_CHANNEL_BINDINGS: u16 = 0x000A;
32
33// Minimal NTLM Type 1 (Negotiate)
34
35/// Anonymous NTLM Type 1 Negotiate token.
36///
37/// Flags encoded (little-endian `0xa0088207`):
38///  NTLMSSP_NEGOTIATE_UNICODE                  (0x00000001)
39///  NTLMSSP_NEGOTIATE_OEM                      (0x00000002)
40///  NTLMSSP_REQUEST_TARGET                     (0x00000004)
41///  NTLMSSP_NEGOTIATE_NTLM                     (0x00000200)
42///  NTLMSSP_NEGOTIATE_ALWAYS_SIGN              (0x00008000)
43///  NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY (0x00080000)
44///  NTLMSSP_NEGOTIATE_128                      (0x20000000)
45///  NTLMSSP_NEGOTIATE_56                       (0x80000000)
46///
47/// NEGOTIATE_VERSION (0x02000000) MUST NOT be set here: MS-NLMP §2.2.1.1
48/// requires an 8-byte Version block when that flag is present, and this
49/// minimal 32-byte token omits it. IIS/HTTP.sys rejects a Type 1 that claims
50/// NEGOTIATE_VERSION without a Version block: it never returns a Type 2
51/// challenge, so the EPA probe cannot see MsvAvChannelBindings and the CA
52/// is silently reported as not ESC8-vulnerable.
53///
54/// Domain and Workstation fields are empty; no version block.
55const NTLM_NEGOTIATE: &[u8] = &[
56    // Signature
57    0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00,
58    // MessageType = 1
59    0x01, 0x00, 0x00, 0x00,
60    // NegotiateFlags LE 0xa0088207 (no NEGOTIATE_VERSION 0x02000000: without a Version block
61    // present, IIS rejects the Type 1 as malformed and never returns a Type 2 challenge).
62    0x07, 0x82, 0x08, 0xa0,
63    // DomainNameFields: empty
64    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
65    // WorkstationFields: empty
66    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
67];
68
69// Status string values matching BloodHound CE expected format.
70pub const STATUS_VULNERABLE_HTTP:  &str = "Vulnerable_NtlmHttpEndpoint";
71pub const STATUS_VULNERABLE_HTTPS: &str = "Vulnerable_NtlmHttpsEndpointWithoutEpa";
72pub const STATUS_NOT_VULN_EPA:     &str = "NotVulnerable_EpaEnabled";
73pub const STATUS_NOT_VULN_PORT:    &str = "NotVulnerable_PortInaccessible";
74
75// Public types
76
77/// Status of a single web-enrollment endpoint (HTTP or HTTPS).
78#[derive(Debug, Clone, PartialEq)]
79pub enum WebEnrollmentStatus {
80    /// Endpoint not reachable, or web enrollment not installed.
81    NotFound,
82    /// Web enrollment is reachable and NTLM auth is available, relay possible.
83    Vulnerable,
84    /// Web enrollment is on HTTPS and EPA/channel binding is enforced, protected.
85    Protected,
86}
87
88// Builder functions for WebEnrollmentEndpoint
89// (impl on an external type would violate the orphan rule)
90
91/// Build a WebEnrollmentEndpoint from a plain-HTTP probe result.
92fn build_http_endpoint(host: &str, vulnerable: bool) -> WebEnrollmentEndpoint {
93    WebEnrollmentEndpoint {
94        result: Some(WebEnrollmentResult {
95            url:                       format!("http://{}/certsrv/", host),
96            enrollment_type:           "WebEnrollmentApplication".to_string(),
97            status: if vulnerable {
98                STATUS_VULNERABLE_HTTP.to_string()
99            } else {
100                STATUS_NOT_VULN_PORT.to_string()
101            },
102            adcs_web_enrollment_http:  vulnerable,
103            adcs_web_enrollment_https: false,
104            adcs_web_enrollment_epa:   false,
105        }),
106        collected:      true,
107        failure_reason: None,
108    }
109}
110
111/// Build a WebEnrollmentEndpoint from an HTTPS probe result.
112fn build_https_endpoint(host: &str, https_status: &WebEnrollmentStatus) -> WebEnrollmentEndpoint {
113    let (status, https, epa) = match https_status {
114        WebEnrollmentStatus::Vulnerable => (STATUS_VULNERABLE_HTTPS.to_string(), true,  false),
115        WebEnrollmentStatus::Protected  => (STATUS_NOT_VULN_EPA.to_string(),     true,  true),
116        WebEnrollmentStatus::NotFound   => (STATUS_NOT_VULN_PORT.to_string(),    false, false),
117    };
118    WebEnrollmentEndpoint {
119        result: Some(WebEnrollmentResult {
120            url:                       format!("https://{}/certsrv/", host),
121            enrollment_type:           "WebEnrollmentApplication".to_string(),
122            status,
123            adcs_web_enrollment_http:  false,
124            adcs_web_enrollment_https: https,
125            adcs_web_enrollment_epa:   epa,
126        }),
127        collected:      true,
128        failure_reason: None,
129    }
130}
131
132/// Full ESC8 probe result for a CA host.
133#[derive(Debug, Clone)]
134pub struct Esc8Result {
135    pub host: String,
136    /// HTTP endpoint status.
137    pub http: WebEnrollmentStatus,
138    /// HTTPS endpoint status (checks EPA via NTLM Type 2 parsing).
139    pub https: WebEnrollmentStatus,
140    /// `true` if either endpoint is relay-able.
141    pub vulnerable: bool,
142    /// Both endpoints (HTTP + HTTPS), ready for JSON serialization.
143    pub endpoints: Vec<WebEnrollmentEndpoint>,
144}
145
146// Public API
147
148/// Run the full ESC8 probe against a CA host (both HTTP and HTTPS).
149///
150/// Returns `None` if the host is completely unreachable on both endpoints.
151pub fn check_esc8(host: &str) -> Option<Esc8Result> {
152    let http  = probe_http(host);
153    let https = probe_https(host);
154
155    if http == WebEnrollmentStatus::NotFound && https == WebEnrollmentStatus::NotFound {
156        return None;
157    }
158
159    let vulnerable = http  == WebEnrollmentStatus::Vulnerable
160        || https == WebEnrollmentStatus::Vulnerable;
161
162    if http == WebEnrollmentStatus::Vulnerable {
163        warn!(
164            "ESC8 detected on {}, Web Enrollment exposed over HTTP without EPA \
165             (NTLM relay possible on http://{}/certsrv/certfnsh.asp)",
166            host, host
167        );
168    }
169    if https == WebEnrollmentStatus::Vulnerable {
170        warn!(
171            "ESC8 detected on {}, Web Enrollment over HTTPS without Channel Binding \
172             (NTLM relay possible on https://{}/certsrv/certfnsh.asp)",
173            host, host
174        );
175    }
176    if https == WebEnrollmentStatus::Protected {
177        debug!("ESC8 HTTPS {}: EPA/Channel Binding enforced, protected", host);
178    }
179
180    let endpoints = vec![
181        build_http_endpoint(host, http == WebEnrollmentStatus::Vulnerable),
182        build_https_endpoint(host, &https),
183    ];
184
185    Some(Esc8Result {
186        host: host.to_string(),
187        http,
188        https,
189        vulnerable,
190        endpoints,
191    })
192}
193
194// Internal probes
195
196/// Probe the plain-HTTP enrollment endpoint.
197///
198/// A `401` response carrying `WWW-Authenticate: NTLM` or `Negotiate` over HTTP
199/// is sufficient to flag ESC8, HTTP provides no channel-binding protection.
200fn probe_http(host: &str) -> WebEnrollmentStatus {
201    let url = format!("http://{}/certsrv/certfnsh.asp", host);
202    debug!("ESC8 HTTP probe: {}", url);
203
204    let client = match Client::builder()
205        .timeout(Duration::from_secs(5))
206        .connect_timeout(Duration::from_secs(3))
207        .redirect(reqwest::redirect::Policy::limited(3))
208        .build()
209    {
210        Ok(c) => c,
211        Err(_) => return WebEnrollmentStatus::NotFound,
212    };
213
214    let response = match client.head(&url).send() {
215        Ok(r) => r,
216        Err(_) => return WebEnrollmentStatus::NotFound,
217    };
218
219    let status = response.status().as_u16();
220    let has_ntlm = response
221        .headers()
222        .get_all(WWW_AUTHENTICATE)
223        .iter()
224        .any(|v| {
225            let s = v.to_str().unwrap_or("").to_lowercase();
226            s.starts_with("ntlm") || s.starts_with("negotiate")
227        });
228
229    debug!("ESC8 HTTP probe {}: status={} ntlm={}", host, status, has_ntlm);
230
231    if status == 401 && has_ntlm {
232        WebEnrollmentStatus::Vulnerable
233    } else {
234        WebEnrollmentStatus::NotFound
235    }
236}
237
238/// Probe the HTTPS enrollment endpoint and check for EPA (Channel Binding).
239///
240/// Sends a minimal NTLM Type 1 Negotiate. If the server responds with a Type 2
241/// Challenge, parses the `TargetInfo` AvPairs to check for `MsvAvChannelBindings`.
242/// Absent: EPA disabled: relay possible.
243fn probe_https(host: &str) -> WebEnrollmentStatus {
244    let url = format!("https://{}/certsrv/certfnsh.asp", host);
245    debug!("ESC8 HTTPS probe: {}", url);
246
247    let neg_b64    = b64_encode(NTLM_NEGOTIATE);
248    let auth_value = format!("NTLM {}", neg_b64);
249
250    let client = match Client::builder()
251        .timeout(Duration::from_secs(8))
252        .connect_timeout(Duration::from_secs(3))
253        .danger_accept_invalid_certs(true)
254        .build()
255    {
256        Ok(c) => c,
257        Err(_) => return WebEnrollmentStatus::NotFound,
258    };
259
260    let response = match client
261        .get(&url)
262        .header(AUTHORIZATION, &auth_value)
263        .send()
264    {
265        Ok(r) => r,
266        Err(_) => return WebEnrollmentStatus::NotFound,
267    };
268
269    let status = response.status().as_u16();
270    debug!("ESC8 HTTPS probe {}: status={}", host, status);
271
272    if status != 401 {
273        return WebEnrollmentStatus::NotFound;
274    }
275
276    // Find the NTLM Type 2 Challenge token in WWW-Authenticate headers
277    let challenge_token = response
278        .headers()
279        .get_all(WWW_AUTHENTICATE)
280        .iter()
281        .find_map(|v| {
282            let s = v.to_str().unwrap_or("");
283            let lower = s.to_ascii_lowercase();
284            if let Some(rest) = lower.strip_prefix("ntlm ") {
285                let token_b64 = rest.trim();
286                if token_b64.len() > 16 {
287                    let orig = s["ntlm ".len()..].trim();
288                    return b64_decode(orig);
289                }
290            }
291            None
292        });
293
294    match challenge_token {
295        None => {
296            debug!(
297                "ESC8 HTTPS {}: no NTLM challenge received (Kerberos-only or not installed)",
298                host
299            );
300            WebEnrollmentStatus::NotFound
301        }
302        Some(token) => {
303            if parse_epa_channel_bindings(&token) {
304                debug!("ESC8 HTTPS {}: MsvAvChannelBindings present: EPA enforced", host);
305                WebEnrollmentStatus::Protected
306            } else {
307                debug!("ESC8 HTTPS {}: MsvAvChannelBindings absent: EPA disabled", host);
308                WebEnrollmentStatus::Vulnerable
309            }
310        }
311    }
312}
313
314// NTLM Type 2 / EPA parsing
315
316/// Parse an NTLM Type 2 (Challenge) token and return `true` if
317/// `MsvAvChannelBindings` (AvId `0x000A`) is present with a **non-zero** length.
318/// <https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/34a9417d-7cc0-43b0-b61c-1f19740df66f>
319///
320/// NTLM Type 2 layout (all little-endian):
321///
322/// | Offset | Size | Field               |
323/// |--------|------|---------------------|
324/// |  0     |  8   | Signature           |
325/// |  8     |  4   | MessageType = 2     |
326/// | 12     |  8   | TargetNameFields    |
327/// | 20     |  4   | NegotiateFlags      |
328/// | 24     |  8   | ServerChallenge     |
329/// | 32     |  8   | Reserved            |
330/// | 40     |  8   | TargetInfoFields    |
331/// | 48     |  8   | Version (optional)  |
332/// | 56+    |  …   | Payload             |
333///
334/// AvPair layout: `AvId u16 | AvLen u16 | AvValue [u8; AvLen]`
335pub fn parse_epa_channel_bindings(token: &[u8]) -> bool {
336    if token.len() < 48 {
337        debug!("NTLM token too short ({} bytes), cannot parse as Type 2", token.len());
338        return false;
339    }
340
341    if &token[0..8] != b"NTLMSSP\0" {
342        debug!("NTLM signature mismatch");
343        return false;
344    }
345
346    let msg_type = u32::from_le_bytes([token[8], token[9], token[10], token[11]]);
347    if msg_type != 2 {
348        debug!("Not a Type 2 message (MessageType={})", msg_type);
349        return false;
350    }
351
352    let ti_len = u16::from_le_bytes([token[40], token[41]]) as usize;
353    let ti_off = u32::from_le_bytes([token[44], token[45], token[46], token[47]]) as usize;
354
355    if ti_len == 0 {
356        debug!("TargetInfo is empty, no AvPairs to inspect");
357        return false;
358    }
359    if token.len() < ti_off.saturating_add(ti_len) {
360        debug!(
361            "TargetInfo out of bounds (off={}, len={}, token_len={})",
362            ti_off, ti_len, token.len()
363        );
364        return false;
365    }
366
367    let avpairs = &token[ti_off..ti_off + ti_len];
368    debug!("Parsing {} bytes of AvPairs", avpairs.len());
369
370    let mut i = 0;
371    while i + 4 <= avpairs.len() {
372        let av_id  = u16::from_le_bytes([avpairs[i],     avpairs[i + 1]]);
373        let av_len = u16::from_le_bytes([avpairs[i + 2], avpairs[i + 3]]) as usize;
374
375        match av_id {
376            MV_AV_EOL => {
377                debug!("MsvAvEOL reached");
378                break;
379            }
380            MV_AV_CHANNEL_BINDINGS => {
381                debug!("MsvAvChannelBindings found (av_len={})", av_len);
382                return av_len > 0;
383            }
384            other => {
385                debug!("AvPair id=0x{:04x} len={}, skipping", other, av_len);
386                i += 4 + av_len;
387            }
388        }
389    }
390
391    false
392}
393
394// Tests
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    // Test helpers
401
402    fn build_type2(avpairs: &[u8]) -> Vec<u8> {
403        let mut t = Vec::new();
404        t.extend_from_slice(b"NTLMSSP\0");
405        t.extend_from_slice(&2u32.to_le_bytes());
406        t.extend_from_slice(&0u16.to_le_bytes());
407        t.extend_from_slice(&0u16.to_le_bytes());
408        t.extend_from_slice(&56u32.to_le_bytes());
409        t.extend_from_slice(&0u32.to_le_bytes());
410        t.extend_from_slice(&[0x01u8; 8]);
411        t.extend_from_slice(&[0u8; 8]);
412        let ti_len = avpairs.len() as u16;
413        t.extend_from_slice(&ti_len.to_le_bytes());
414        t.extend_from_slice(&ti_len.to_le_bytes());
415        t.extend_from_slice(&56u32.to_le_bytes());
416        t.extend_from_slice(&[0u8; 8]);
417        t.extend_from_slice(avpairs);
418        t
419    }
420
421    fn avpairs_with_channel_bindings(value: &[u8]) -> Vec<u8> {
422        let mut p = Vec::new();
423        p.extend_from_slice(&MV_AV_CHANNEL_BINDINGS.to_le_bytes());
424        p.extend_from_slice(&(value.len() as u16).to_le_bytes());
425        p.extend_from_slice(value);
426        p.extend_from_slice(&MV_AV_EOL.to_le_bytes());
427        p.extend_from_slice(&0u16.to_le_bytes());
428        p
429    }
430
431    fn avpairs_without_channel_bindings() -> Vec<u8> {
432        let name: Vec<u8> = "SERVER"
433            .encode_utf16()
434            .flat_map(|u| u.to_le_bytes())
435            .collect();
436        let mut p = Vec::new();
437        p.extend_from_slice(&0x0001u16.to_le_bytes());
438        p.extend_from_slice(&(name.len() as u16).to_le_bytes());
439        p.extend_from_slice(&name);
440        p.extend_from_slice(&MV_AV_EOL.to_le_bytes());
441        p.extend_from_slice(&0u16.to_le_bytes());
442        p
443    }
444
445    // parse_epa_channel_bindings
446
447    #[test]
448    fn epa_present_with_non_zero_value() {
449        let cbt = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
450                   0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
451        let token = build_type2(&avpairs_with_channel_bindings(&cbt));
452        assert!(parse_epa_channel_bindings(&token));
453    }
454
455    #[test]
456    fn epa_present_but_zero_length() {
457        let token = build_type2(&avpairs_with_channel_bindings(&[]));
458        assert!(!parse_epa_channel_bindings(&token));
459    }
460
461    #[test]
462    fn epa_absent_from_avpairs() {
463        let token = build_type2(&avpairs_without_channel_bindings());
464        assert!(!parse_epa_channel_bindings(&token));
465    }
466
467    #[test]
468    fn epa_multiple_avpairs_with_channel_bindings_last() {
469        let name: Vec<u8> = "DC01"
470            .encode_utf16()
471            .flat_map(|u| u.to_le_bytes())
472            .collect();
473        let cbt = [0xAA, 0xBB, 0xCC, 0xDD];
474        let mut avpairs = Vec::new();
475        avpairs.extend_from_slice(&0x0001u16.to_le_bytes());
476        avpairs.extend_from_slice(&(name.len() as u16).to_le_bytes());
477        avpairs.extend_from_slice(&name);
478        avpairs.extend_from_slice(&MV_AV_CHANNEL_BINDINGS.to_le_bytes());
479        avpairs.extend_from_slice(&(cbt.len() as u16).to_le_bytes());
480        avpairs.extend_from_slice(&cbt);
481        avpairs.extend_from_slice(&MV_AV_EOL.to_le_bytes());
482        avpairs.extend_from_slice(&0u16.to_le_bytes());
483        let token = build_type2(&avpairs);
484        assert!(parse_epa_channel_bindings(&token));
485    }
486
487    #[test]
488    fn epa_empty_avpairs() {
489        let token = build_type2(&[]);
490        assert!(!parse_epa_channel_bindings(&token));
491    }
492
493    // Structural validation
494
495    #[test]
496    fn token_too_short_returns_false() {
497        assert!(!parse_epa_channel_bindings(&[0u8; 10]));
498        assert!(!parse_epa_channel_bindings(&[]));
499    }
500
501    #[test]
502    fn invalid_signature_returns_false() {
503        let mut token = build_type2(&avpairs_without_channel_bindings());
504        token[0] = 0xFF;
505        assert!(!parse_epa_channel_bindings(&token));
506    }
507
508    #[test]
509    fn wrong_message_type_returns_false() {
510        let mut token = build_type2(&avpairs_without_channel_bindings());
511        token[8]  = 0x01;
512        token[9]  = 0x00;
513        token[10] = 0x00;
514        token[11] = 0x00;
515        assert!(!parse_epa_channel_bindings(&token));
516    }
517
518    #[test]
519    fn target_info_offset_out_of_bounds_returns_false() {
520        let avpairs = avpairs_without_channel_bindings();
521        let mut token = build_type2(&avpairs);
522        let bad_offset = (token.len() + 1024) as u32;
523        token[44..48].copy_from_slice(&bad_offset.to_le_bytes());
524        assert!(!parse_epa_channel_bindings(&token));
525    }
526
527    // Base64 helpers
528
529    #[test]
530    fn base64_roundtrip_ntlm_negotiate() {
531        let encoded = b64_encode(NTLM_NEGOTIATE);
532        let decoded = b64_decode(&encoded).expect("base64_decode should succeed");
533        assert_eq!(NTLM_NEGOTIATE, decoded.as_slice());
534    }
535
536    #[test]
537    fn base64_known_vector() {
538        assert_eq!(b64_encode(b"Man"), "TWFu");
539        assert_eq!(b64_decode("TWFu"), Some(b"Man".to_vec()));
540    }
541
542    #[test]
543    fn base64_with_padding() {
544        assert_eq!(b64_encode(b"Ma"), "TWE=");
545        assert_eq!(b64_decode("TWE="), Some(b"Ma".to_vec()));
546        assert_eq!(b64_encode(b"M"), "TQ==");
547        assert_eq!(b64_decode("TQ=="), Some(b"M".to_vec()));
548    }
549
550    #[test]
551    fn base64_decode_invalid_char_returns_none() {
552        assert_eq!(b64_decode("TQ!Q"), None);
553    }
554
555    #[test]
556    fn base64_decode_empty_input() {
557        assert_eq!(b64_decode(""), Some(vec![]));
558    }
559
560    // Network probe (non-routable, expected to return None)
561
562    #[test]
563    fn unreachable_host_returns_none() {
564        let result = check_esc8("192.0.2.1");
565        assert!(result.is_none(), "Non-routable host must return None");
566    }
567
568    // WebEnrollmentEndpoint builders
569
570    #[test]
571    fn from_http_vulnerable() {
572        let ep = build_http_endpoint("ca.corp.local", true);
573        let r  = ep.result.as_ref().unwrap();
574        assert_eq!(r.status, STATUS_VULNERABLE_HTTP);
575        assert!(r.adcs_web_enrollment_http);
576        assert!(!r.adcs_web_enrollment_https);
577        assert!(!r.adcs_web_enrollment_epa);
578        assert!(ep.collected);
579        assert!(ep.failure_reason.is_none());
580    }
581
582    #[test]
583    fn from_http_not_found() {
584        let ep = build_http_endpoint("ca.corp.local", false);
585        let r  = ep.result.as_ref().unwrap();
586        assert_eq!(r.status, STATUS_NOT_VULN_PORT);
587        assert!(!r.adcs_web_enrollment_http);
588    }
589
590    #[test]
591    fn from_https_vulnerable() {
592        let ep = build_https_endpoint("ca.corp.local", &WebEnrollmentStatus::Vulnerable);
593        let r  = ep.result.as_ref().unwrap();
594        assert_eq!(r.status, STATUS_VULNERABLE_HTTPS);
595        assert!(!r.adcs_web_enrollment_http);
596        assert!(r.adcs_web_enrollment_https);
597        assert!(!r.adcs_web_enrollment_epa);
598    }
599
600    #[test]
601    fn from_https_protected() {
602        let ep = build_https_endpoint("ca.corp.local", &WebEnrollmentStatus::Protected);
603        let r  = ep.result.as_ref().unwrap();
604        assert_eq!(r.status, STATUS_NOT_VULN_EPA);
605        assert!(r.adcs_web_enrollment_https);
606        assert!(r.adcs_web_enrollment_epa);
607    }
608
609    #[test]
610    fn from_https_not_found() {
611        let ep = build_https_endpoint("ca.corp.local", &WebEnrollmentStatus::NotFound);
612        let r  = ep.result.as_ref().unwrap();
613        assert_eq!(r.status, STATUS_NOT_VULN_PORT);
614        assert!(!r.adcs_web_enrollment_https);
615        assert!(!r.adcs_web_enrollment_epa);
616    }
617}