1use crate::objects::enterpriseca::{WebEnrollmentEndpoint, WebEnrollmentResult};
33use crate::utils::b64::{b64_decode, b64_encode};
34use log::{debug, warn};
35use reqwest::blocking::Client;
36use reqwest::header::{AUTHORIZATION, WWW_AUTHENTICATE};
37use std::net::{TcpStream, ToSocketAddrs};
38use std::time::Duration;
39
40const MV_AV_EOL: u16 = 0x0000;
44
45const MV_AV_CHANNEL_BINDINGS: u16 = 0x000A;
47
48const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
52const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
54const HTTP_TIMEOUT: Duration = Duration::from_secs(5);
56const HTTPS_TIMEOUT: Duration = Duration::from_secs(8);
58
59const NTLM_NEGOTIATE: &[u8] = &[
82 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00,
84 0x01, 0x00, 0x00, 0x00,
86 0x07, 0x82, 0x08, 0xa0,
89 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
91 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
93];
94
95pub const STATUS_VULNERABLE_HTTP: &str = "Vulnerable_NtlmHttpEndpoint";
97pub const STATUS_VULNERABLE_HTTPS: &str = "Vulnerable_NtlmHttpsEndpointWithoutEpa";
98pub const STATUS_NOT_VULN_EPA: &str = "NotVulnerable_EpaEnabled";
99pub const STATUS_NOT_VULN_PORT: &str = "NotVulnerable_PortInaccessible";
100
101const TYPE_WEB_ENROLLMENT: &str = "WebEnrollmentApplication";
103
104fn display_url(scheme: &str, host: &str) -> String {
107 format!("{}://{}/certsrv/", scheme, host)
108}
109
110fn probe_url(scheme: &str, host: &str) -> String {
112 format!("{}://{}/certsrv/certfnsh.asp", scheme, host)
113}
114
115#[derive(Debug, Clone, PartialEq)]
119pub enum WebEnrollmentStatus {
120 NotFound,
122 Vulnerable,
124 Protected,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub enum ProbeOutcome {
131 Reached(WebEnrollmentStatus),
133 PortClosed,
135 Failed(String),
137}
138
139fn outcome_status(outcome: &ProbeOutcome) -> WebEnrollmentStatus {
141 match outcome {
142 ProbeOutcome::Reached(status) => status.clone(),
143 _ => WebEnrollmentStatus::NotFound,
144 }
145}
146
147fn build_non_result(
152 url: String,
153 outcome: &ProbeOutcome,
154) -> Option<WebEnrollmentEndpoint> {
155 match outcome {
156 ProbeOutcome::PortClosed => Some(WebEnrollmentEndpoint {
157 result: Some(WebEnrollmentResult {
158 url,
159 enrollment_type: TYPE_WEB_ENROLLMENT.to_string(),
160 status: STATUS_NOT_VULN_PORT.to_string(),
161 adcs_web_enrollment_http: false,
162 adcs_web_enrollment_https: false,
163 adcs_web_enrollment_epa: false,
164 }),
165 collected: true,
166 failure_reason: None,
167 }),
168 ProbeOutcome::Failed(reason) => Some(WebEnrollmentEndpoint {
169 result: None,
170 collected: false,
171 failure_reason: Some(reason.clone()),
172 }),
173 ProbeOutcome::Reached(_) => None,
174 }
175}
176
177fn build_http_endpoint(host: &str, outcome: &ProbeOutcome) -> WebEnrollmentEndpoint {
179 let url = display_url("http", host);
180
181 if let Some(ep) = build_non_result(url.clone(), outcome) {
182 return ep;
183 }
184
185 let vulnerable = outcome_status(outcome) == WebEnrollmentStatus::Vulnerable;
186
187 WebEnrollmentEndpoint {
188 result: Some(WebEnrollmentResult {
189 url,
190 enrollment_type: TYPE_WEB_ENROLLMENT.to_string(),
191 status: if vulnerable {
192 STATUS_VULNERABLE_HTTP.to_string()
193 } else {
194 STATUS_NOT_VULN_PORT.to_string()
195 },
196 adcs_web_enrollment_http: vulnerable,
197 adcs_web_enrollment_https: false,
198 adcs_web_enrollment_epa: false,
199 }),
200 collected: true,
201 failure_reason: None,
202 }
203}
204
205fn build_https_endpoint(host: &str, outcome: &ProbeOutcome) -> WebEnrollmentEndpoint {
207 let url = display_url("https", host);
208
209 if let Some(ep) = build_non_result(url.clone(), outcome) {
210 return ep;
211 }
212
213 let (status, https, epa) = match outcome_status(outcome) {
214 WebEnrollmentStatus::Vulnerable => (STATUS_VULNERABLE_HTTPS.to_string(), true, false),
215 WebEnrollmentStatus::Protected => (STATUS_NOT_VULN_EPA.to_string(), true, true),
216 WebEnrollmentStatus::NotFound => (STATUS_NOT_VULN_PORT.to_string(), false, false),
217 };
218
219 WebEnrollmentEndpoint {
220 result: Some(WebEnrollmentResult {
221 url,
222 enrollment_type: TYPE_WEB_ENROLLMENT.to_string(),
223 status,
224 adcs_web_enrollment_http: false,
225 adcs_web_enrollment_https: https,
226 adcs_web_enrollment_epa: epa,
227 }),
228 collected: true,
229 failure_reason: None,
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct Esc8Result {
236 pub host: String,
237 pub http: WebEnrollmentStatus,
239 pub https: WebEnrollmentStatus,
241 pub vulnerable: bool,
243 pub endpoints: Vec<WebEnrollmentEndpoint>,
246}
247
248pub fn check_esc8(host: &str) -> Esc8Result {
255 let http_outcome = probe_http(host);
256 let https_outcome = probe_https(host);
257
258 let http = outcome_status(&http_outcome);
259 let https = outcome_status(&https_outcome);
260
261 let vulnerable = http == WebEnrollmentStatus::Vulnerable
262 || https == WebEnrollmentStatus::Vulnerable;
263
264 if http == WebEnrollmentStatus::Vulnerable {
265 warn!(
266 "ESC8 detected on {}, Web Enrollment exposed over HTTP without EPA \
267 (NTLM relay possible on {})",
268 host,
269 probe_url("http", host)
270 );
271 }
272 if https == WebEnrollmentStatus::Vulnerable {
273 warn!(
274 "ESC8 detected on {}, Web Enrollment over HTTPS without Channel Binding \
275 (NTLM relay possible on {})",
276 host,
277 probe_url("https", host)
278 );
279 }
280 if https == WebEnrollmentStatus::Protected {
281 debug!("ESC8 HTTPS {}: EPA/Channel Binding enforced, protected", host);
282 }
283 if let ProbeOutcome::Failed(ref reason) = http_outcome {
284 debug!("ESC8 HTTP {} not collected: {}", host, reason);
285 }
286 if let ProbeOutcome::Failed(ref reason) = https_outcome {
287 debug!("ESC8 HTTPS {} not collected: {}", host, reason);
288 }
289
290 let endpoints = vec![
291 build_http_endpoint(host, &http_outcome),
292 build_https_endpoint(host, &https_outcome),
293 ];
294
295 Esc8Result {
296 host: host.to_string(),
297 http,
298 https,
299 vulnerable,
300 endpoints,
301 }
302}
303
304enum PortState {
308 Open,
310 Closed,
312 Unresolved(String),
314}
315
316fn check_port(host: &str, port: u16) -> PortState {
321 let addrs = match (host, port).to_socket_addrs() {
322 Ok(a) => a.collect::<Vec<_>>(),
323 Err(e) => {
324 return PortState::Unresolved(format!(
325 "DNS resolution failed for {}:{}: {}",
326 host, port, e
327 ));
328 }
329 };
330
331 if addrs.is_empty() {
332 return PortState::Unresolved(format!("no address resolved for {}:{}", host, port));
333 }
334
335 for addr in &addrs {
336 match TcpStream::connect_timeout(addr, TCP_CONNECT_TIMEOUT) {
337 Ok(_) => {
338 debug!("ESC8 port check {}:{} open ({})", host, port, addr);
339 return PortState::Open;
340 }
341 Err(e) => debug!("ESC8 port check {} unreachable: {}", addr, e),
342 }
343 }
344
345 PortState::Closed
346}
347
348fn probe_http(host: &str) -> ProbeOutcome {
358 let url = probe_url("http", host);
359 debug!("ESC8 HTTP probe: {}", url);
360
361 match check_port(host, 80) {
362 PortState::Open => {}
363 PortState::Closed => return ProbeOutcome::PortClosed,
364 PortState::Unresolved(reason) => return ProbeOutcome::Failed(reason),
365 }
366
367 let client = match Client::builder()
368 .timeout(HTTP_TIMEOUT)
369 .connect_timeout(HTTP_CONNECT_TIMEOUT)
370 .redirect(reqwest::redirect::Policy::limited(3))
371 .build()
372 {
373 Ok(c) => c,
374 Err(e) => {
375 return ProbeOutcome::Failed(format!("failed to build HTTP client for {}: {}", url, e));
376 }
377 };
378
379 let response = match client.head(&url).send() {
380 Ok(r) => r,
381 Err(e) => {
382 return ProbeOutcome::Failed(format!("HTTP request to {} failed: {}", url, e));
383 }
384 };
385
386 let status = response.status();
387 let code = status.as_u16();
388 let has_ntlm = response
389 .headers()
390 .get_all(WWW_AUTHENTICATE)
391 .iter()
392 .any(|v| {
393 let s = v.to_str().unwrap_or("").to_lowercase();
394 s.starts_with("ntlm") || s.starts_with("negotiate")
395 });
396
397 debug!("ESC8 HTTP probe {}: status={} ntlm={}", host, code, has_ntlm);
398
399 if code == 401 {
400 return if has_ntlm {
401 ProbeOutcome::Reached(WebEnrollmentStatus::Vulnerable)
402 } else {
403 ProbeOutcome::Reached(WebEnrollmentStatus::NotFound)
405 };
406 }
407
408 if status.is_success() || status.is_redirection() {
409 return ProbeOutcome::Reached(WebEnrollmentStatus::NotFound);
411 }
412
413 ProbeOutcome::Failed(format!(
414 "Response status code does not indicate success: {} ({}) for {}",
415 code,
416 status.canonical_reason().unwrap_or("Unknown"),
417 url
418 ))
419}
420
421fn probe_https(host: &str) -> ProbeOutcome {
427 let url = probe_url("https", host);
428 debug!("ESC8 HTTPS probe: {}", url);
429
430 match check_port(host, 443) {
431 PortState::Open => {}
432 PortState::Closed => return ProbeOutcome::PortClosed,
433 PortState::Unresolved(reason) => return ProbeOutcome::Failed(reason),
434 }
435
436 let neg_b64 = b64_encode(NTLM_NEGOTIATE);
437 let auth_value = format!("NTLM {}", neg_b64);
438
439 let client = match Client::builder()
440 .timeout(HTTPS_TIMEOUT)
441 .connect_timeout(HTTP_CONNECT_TIMEOUT)
442 .danger_accept_invalid_certs(true)
443 .build()
444 {
445 Ok(c) => c,
446 Err(e) => {
447 return ProbeOutcome::Failed(format!("failed to build HTTPS client for {}: {}", url, e));
448 }
449 };
450
451 let response = match client.get(&url).header(AUTHORIZATION, &auth_value).send() {
452 Ok(r) => r,
453 Err(e) => {
454 return ProbeOutcome::Failed(format!("HTTPS request to {} failed: {}", url, e));
455 }
456 };
457
458 let status = response.status();
459 let code = status.as_u16();
460 debug!("ESC8 HTTPS probe {}: status={}", host, code);
461
462 if code != 401 {
463 if status.is_success() || status.is_redirection() {
464 return ProbeOutcome::Reached(WebEnrollmentStatus::NotFound);
465 }
466 return ProbeOutcome::Failed(format!(
467 "Response status code does not indicate success: {} ({}) for {}",
468 code,
469 status.canonical_reason().unwrap_or("Unknown"),
470 url
471 ));
472 }
473
474 let challenge_token = response
476 .headers()
477 .get_all(WWW_AUTHENTICATE)
478 .iter()
479 .find_map(|v| {
480 let s = v.to_str().unwrap_or("");
481 let lower = s.to_ascii_lowercase();
482 if let Some(rest) = lower.strip_prefix("ntlm ") {
483 let token_b64 = rest.trim();
484 if token_b64.len() > 16 {
485 let orig = s["ntlm ".len()..].trim();
486 return b64_decode(orig);
487 }
488 }
489 None
490 });
491
492 match challenge_token {
493 None => {
494 debug!(
495 "ESC8 HTTPS {}: no NTLM challenge received (Kerberos-only or not installed)",
496 host
497 );
498 ProbeOutcome::Reached(WebEnrollmentStatus::NotFound)
499 }
500 Some(token) => {
501 if parse_epa_channel_bindings(&token) {
502 debug!("ESC8 HTTPS {}: MsvAvChannelBindings present: EPA enforced", host);
503 ProbeOutcome::Reached(WebEnrollmentStatus::Protected)
504 } else {
505 debug!("ESC8 HTTPS {}: MsvAvChannelBindings absent: EPA disabled", host);
506 ProbeOutcome::Reached(WebEnrollmentStatus::Vulnerable)
507 }
508 }
509 }
510}
511
512pub fn parse_epa_channel_bindings(token: &[u8]) -> bool {
534 if token.len() < 48 {
535 debug!("NTLM token too short ({} bytes), cannot parse as Type 2", token.len());
536 return false;
537 }
538
539 if &token[0..8] != b"NTLMSSP\0" {
540 debug!("NTLM signature mismatch");
541 return false;
542 }
543
544 let msg_type = u32::from_le_bytes([token[8], token[9], token[10], token[11]]);
545 if msg_type != 2 {
546 debug!("Not a Type 2 message (MessageType={})", msg_type);
547 return false;
548 }
549
550 let ti_len = u16::from_le_bytes([token[40], token[41]]) as usize;
551 let ti_off = u32::from_le_bytes([token[44], token[45], token[46], token[47]]) as usize;
552
553 if ti_len == 0 {
554 debug!("TargetInfo is empty, no AvPairs to inspect");
555 return false;
556 }
557 if token.len() < ti_off.saturating_add(ti_len) {
558 debug!(
559 "TargetInfo out of bounds (off={}, len={}, token_len={})",
560 ti_off, ti_len, token.len()
561 );
562 return false;
563 }
564
565 let avpairs = &token[ti_off..ti_off + ti_len];
566 debug!("Parsing {} bytes of AvPairs", avpairs.len());
567
568 let mut i = 0;
569 while i + 4 <= avpairs.len() {
570 let av_id = u16::from_le_bytes([avpairs[i], avpairs[i + 1]]);
571 let av_len = u16::from_le_bytes([avpairs[i + 2], avpairs[i + 3]]) as usize;
572
573 match av_id {
574 MV_AV_EOL => {
575 debug!("MsvAvEOL reached");
576 break;
577 }
578 MV_AV_CHANNEL_BINDINGS => {
579 debug!("MsvAvChannelBindings found (av_len={})", av_len);
580 return av_len > 0;
581 }
582 other => {
583 debug!("AvPair id=0x{:04x} len={}, skipping", other, av_len);
584 i += 4 + av_len;
585 }
586 }
587 }
588
589 false
590}
591
592#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn build_type2(avpairs: &[u8]) -> Vec<u8> {
601 let mut t = Vec::new();
602 t.extend_from_slice(b"NTLMSSP\0");
603 t.extend_from_slice(&2u32.to_le_bytes());
604 t.extend_from_slice(&0u16.to_le_bytes());
605 t.extend_from_slice(&0u16.to_le_bytes());
606 t.extend_from_slice(&56u32.to_le_bytes());
607 t.extend_from_slice(&0u32.to_le_bytes());
608 t.extend_from_slice(&[0x01u8; 8]);
609 t.extend_from_slice(&[0u8; 8]);
610 let ti_len = avpairs.len() as u16;
611 t.extend_from_slice(&ti_len.to_le_bytes());
612 t.extend_from_slice(&ti_len.to_le_bytes());
613 t.extend_from_slice(&56u32.to_le_bytes());
614 t.extend_from_slice(&[0u8; 8]);
615 t.extend_from_slice(avpairs);
616 t
617 }
618
619 fn avpairs_with_channel_bindings(value: &[u8]) -> Vec<u8> {
620 let mut p = Vec::new();
621 p.extend_from_slice(&MV_AV_CHANNEL_BINDINGS.to_le_bytes());
622 p.extend_from_slice(&(value.len() as u16).to_le_bytes());
623 p.extend_from_slice(value);
624 p.extend_from_slice(&MV_AV_EOL.to_le_bytes());
625 p.extend_from_slice(&0u16.to_le_bytes());
626 p
627 }
628
629 fn avpairs_without_channel_bindings() -> Vec<u8> {
630 let name: Vec<u8> = "SERVER"
631 .encode_utf16()
632 .flat_map(|u| u.to_le_bytes())
633 .collect();
634 let mut p = Vec::new();
635 p.extend_from_slice(&0x0001u16.to_le_bytes());
636 p.extend_from_slice(&(name.len() as u16).to_le_bytes());
637 p.extend_from_slice(&name);
638 p.extend_from_slice(&MV_AV_EOL.to_le_bytes());
639 p.extend_from_slice(&0u16.to_le_bytes());
640 p
641 }
642
643 #[test]
646 fn epa_present_with_non_zero_value() {
647 let cbt = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
648 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
649 let token = build_type2(&avpairs_with_channel_bindings(&cbt));
650 assert!(parse_epa_channel_bindings(&token));
651 }
652
653 #[test]
654 fn epa_present_but_zero_length() {
655 let token = build_type2(&avpairs_with_channel_bindings(&[]));
656 assert!(!parse_epa_channel_bindings(&token));
657 }
658
659 #[test]
660 fn epa_absent_from_avpairs() {
661 let token = build_type2(&avpairs_without_channel_bindings());
662 assert!(!parse_epa_channel_bindings(&token));
663 }
664
665 #[test]
666 fn epa_multiple_avpairs_with_channel_bindings_last() {
667 let name: Vec<u8> = "DC01"
668 .encode_utf16()
669 .flat_map(|u| u.to_le_bytes())
670 .collect();
671 let cbt = [0xAA, 0xBB, 0xCC, 0xDD];
672 let mut avpairs = Vec::new();
673 avpairs.extend_from_slice(&0x0001u16.to_le_bytes());
674 avpairs.extend_from_slice(&(name.len() as u16).to_le_bytes());
675 avpairs.extend_from_slice(&name);
676 avpairs.extend_from_slice(&MV_AV_CHANNEL_BINDINGS.to_le_bytes());
677 avpairs.extend_from_slice(&(cbt.len() as u16).to_le_bytes());
678 avpairs.extend_from_slice(&cbt);
679 avpairs.extend_from_slice(&MV_AV_EOL.to_le_bytes());
680 avpairs.extend_from_slice(&0u16.to_le_bytes());
681 let token = build_type2(&avpairs);
682 assert!(parse_epa_channel_bindings(&token));
683 }
684
685 #[test]
686 fn epa_empty_avpairs() {
687 let token = build_type2(&[]);
688 assert!(!parse_epa_channel_bindings(&token));
689 }
690
691 #[test]
694 fn token_too_short_returns_false() {
695 assert!(!parse_epa_channel_bindings(&[0u8; 10]));
696 assert!(!parse_epa_channel_bindings(&[]));
697 }
698
699 #[test]
700 fn invalid_signature_returns_false() {
701 let mut token = build_type2(&avpairs_without_channel_bindings());
702 token[0] = 0xFF;
703 assert!(!parse_epa_channel_bindings(&token));
704 }
705
706 #[test]
707 fn wrong_message_type_returns_false() {
708 let mut token = build_type2(&avpairs_without_channel_bindings());
709 token[8] = 0x01;
710 token[9] = 0x00;
711 token[10] = 0x00;
712 token[11] = 0x00;
713 assert!(!parse_epa_channel_bindings(&token));
714 }
715
716 #[test]
717 fn target_info_offset_out_of_bounds_returns_false() {
718 let avpairs = avpairs_without_channel_bindings();
719 let mut token = build_type2(&avpairs);
720 let bad_offset = (token.len() + 1024) as u32;
721 token[44..48].copy_from_slice(&bad_offset.to_le_bytes());
722 assert!(!parse_epa_channel_bindings(&token));
723 }
724
725 #[test]
728 fn base64_roundtrip_ntlm_negotiate() {
729 let encoded = b64_encode(NTLM_NEGOTIATE);
730 let decoded = b64_decode(&encoded).expect("base64_decode should succeed");
731 assert_eq!(NTLM_NEGOTIATE, decoded.as_slice());
732 }
733
734 #[test]
735 fn base64_known_vector() {
736 assert_eq!(b64_encode(b"Man"), "TWFu");
737 assert_eq!(b64_decode("TWFu"), Some(b"Man".to_vec()));
738 }
739
740 #[test]
741 fn base64_with_padding() {
742 assert_eq!(b64_encode(b"Ma"), "TWE=");
743 assert_eq!(b64_decode("TWE="), Some(b"Ma".to_vec()));
744 assert_eq!(b64_encode(b"M"), "TQ==");
745 assert_eq!(b64_decode("TQ=="), Some(b"M".to_vec()));
746 }
747
748 #[test]
749 fn base64_decode_invalid_char_returns_none() {
750 assert_eq!(b64_decode("TQ!Q"), None);
751 }
752
753 #[test]
754 fn base64_decode_empty_input() {
755 assert_eq!(b64_decode(""), Some(vec![]));
756 }
757
758 #[test]
761 fn urls_match_sharphound_shape() {
762 assert_eq!(display_url("http", "ca.corp.local"), "http://ca.corp.local/certsrv/");
763 assert_eq!(
764 probe_url("https", "ca.corp.local"),
765 "https://ca.corp.local/certsrv/certfnsh.asp"
766 );
767 }
768
769 #[test]
775 fn unreachable_host_reports_two_inaccessible_endpoints() {
776 let result = check_esc8("192.0.2.1");
777
778 assert_eq!(result.endpoints.len(), 2, "both endpoints must be reported");
779 assert!(!result.vulnerable, "non-routable host must not be flagged");
780 assert_eq!(result.http, WebEnrollmentStatus::NotFound);
781 assert_eq!(result.https, WebEnrollmentStatus::NotFound);
782
783 for ep in &result.endpoints {
784 assert!(ep.collected, "a closed port is collected data");
785 assert!(ep.failure_reason.is_none());
786 assert_eq!(ep.result.as_ref().unwrap().status, STATUS_NOT_VULN_PORT);
787 }
788 }
789
790 #[test]
793 fn from_http_vulnerable() {
794 let ep = build_http_endpoint(
795 "ca.corp.local",
796 &ProbeOutcome::Reached(WebEnrollmentStatus::Vulnerable),
797 );
798 let r = ep.result.as_ref().unwrap();
799 assert_eq!(r.url, "http://ca.corp.local/certsrv/");
800 assert_eq!(r.enrollment_type, TYPE_WEB_ENROLLMENT);
801 assert_eq!(r.status, STATUS_VULNERABLE_HTTP);
802 assert!(r.adcs_web_enrollment_http);
803 assert!(!r.adcs_web_enrollment_https);
804 assert!(!r.adcs_web_enrollment_epa);
805 assert!(ep.collected);
806 assert!(ep.failure_reason.is_none());
807 }
808
809 #[test]
810 fn from_http_reached_but_not_exposed() {
811 let ep = build_http_endpoint(
812 "ca.corp.local",
813 &ProbeOutcome::Reached(WebEnrollmentStatus::NotFound),
814 );
815 let r = ep.result.as_ref().unwrap();
816 assert_eq!(r.status, STATUS_NOT_VULN_PORT);
817 assert!(!r.adcs_web_enrollment_http);
818 assert!(ep.collected);
819 }
820
821 #[test]
823 fn from_http_port_closed() {
824 let ep = build_http_endpoint("ca.corp.local", &ProbeOutcome::PortClosed);
825 let r = ep.result.as_ref().unwrap();
826 assert_eq!(r.status, STATUS_NOT_VULN_PORT);
827 assert!(ep.collected);
828 assert!(ep.failure_reason.is_none());
829 }
830
831 #[test]
834 fn from_http_request_failed() {
835 let ep = build_http_endpoint(
836 "ca.corp.local",
837 &ProbeOutcome::Failed("Response status code does not indicate success: 404".into()),
838 );
839 assert!(ep.result.is_none());
840 assert!(!ep.collected);
841 assert!(ep.failure_reason.as_ref().unwrap().contains("404"));
842 }
843
844 #[test]
845 fn from_https_vulnerable() {
846 let ep = build_https_endpoint(
847 "ca.corp.local",
848 &ProbeOutcome::Reached(WebEnrollmentStatus::Vulnerable),
849 );
850 let r = ep.result.as_ref().unwrap();
851 assert_eq!(r.url, "https://ca.corp.local/certsrv/");
852 assert_eq!(r.status, STATUS_VULNERABLE_HTTPS);
853 assert!(!r.adcs_web_enrollment_http);
854 assert!(r.adcs_web_enrollment_https);
855 assert!(!r.adcs_web_enrollment_epa);
856 }
857
858 #[test]
859 fn from_https_protected() {
860 let ep = build_https_endpoint(
861 "ca.corp.local",
862 &ProbeOutcome::Reached(WebEnrollmentStatus::Protected),
863 );
864 let r = ep.result.as_ref().unwrap();
865 assert_eq!(r.status, STATUS_NOT_VULN_EPA);
866 assert!(r.adcs_web_enrollment_https);
867 assert!(r.adcs_web_enrollment_epa);
868 }
869
870 #[test]
871 fn from_https_port_closed() {
872 let ep = build_https_endpoint("ca.corp.local", &ProbeOutcome::PortClosed);
873 let r = ep.result.as_ref().unwrap();
874 assert_eq!(r.status, STATUS_NOT_VULN_PORT);
875 assert!(!r.adcs_web_enrollment_https);
876 assert!(!r.adcs_web_enrollment_epa);
877 assert!(ep.collected);
878 }
879
880 #[test]
881 fn from_https_request_failed() {
882 let ep = build_https_endpoint(
883 "ca.corp.local",
884 &ProbeOutcome::Failed("TLS handshake failed".into()),
885 );
886 assert!(ep.result.is_none());
887 assert!(!ep.collected);
888 assert!(ep.failure_reason.as_ref().unwrap().contains("TLS handshake failed"));
889 }
890}