Add CORS RFC-1918 issue

Bug: chromium:1128885
Change-Id: Ia4fc5ee0fd03ad568eccc374db787dc705ee3cef
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2687501
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: Wolfgang Beyer <wolfi@chromium.org>
diff --git a/all_devtools_files.gni b/all_devtools_files.gni
index 7b9b1af..586f48c 100644
--- a/all_devtools_files.gni
+++ b/all_devtools_files.gni
@@ -247,6 +247,7 @@
   "front_end/issues/descriptions/heavyAd.md",
   "front_end/issues/descriptions/mixedContent.md",
   "front_end/issues/descriptions/sharedArrayBuffer.md",
+  "front_end/issues/descriptions/corsInsecurePrivateNetwork.md",
   "front_end/issues/descriptions/SameSiteExcludeContextDowngradeRead.md",
   "front_end/issues/descriptions/SameSiteExcludeContextDowngradeSet.md",
   "front_end/issues/descriptions/SameSiteExcludeNavigationContextDowngrade.md",
diff --git a/all_devtools_modules.gni b/all_devtools_modules.gni
index 2c4e844..8150102 100644
--- a/all_devtools_modules.gni
+++ b/all_devtools_modules.gni
@@ -250,6 +250,7 @@
   "issues/CSPViolationsListView.js",
   "issues/CSPViolationsView.js",
   "issues/ComboBoxOfCheckBoxes.js",
+  "issues/CorsIssueDetailsView.js",
   "issues/IssueAggregator.js",
   "issues/IssueRevealer.js",
   "issues/IssueView.js",
@@ -467,6 +468,7 @@
   "sdk/Cookie.js",
   "sdk/CookieModel.js",
   "sdk/CookieParser.js",
+  "sdk/CorsIssue.js",
   "sdk/CrossOriginEmbedderPolicyIssue.js",
   "sdk/DOMDebuggerModel.js",
   "sdk/DOMModel.js",
diff --git a/devtools_grd_files.gni b/devtools_grd_files.gni
index 9656578..eeeaeeb 100644
--- a/devtools_grd_files.gni
+++ b/devtools_grd_files.gni
@@ -681,6 +681,7 @@
   "front_end/issues/CSPViolationsListView.js",
   "front_end/issues/CSPViolationsView.js",
   "front_end/issues/ComboBoxOfCheckBoxes.js",
+  "front_end/issues/CorsIssueDetailsView.js",
   "front_end/issues/IssueAggregator.js",
   "front_end/issues/IssueRevealer.js",
   "front_end/issues/IssueView.js",
@@ -898,6 +899,7 @@
   "front_end/sdk/Cookie.js",
   "front_end/sdk/CookieModel.js",
   "front_end/sdk/CookieParser.js",
+  "front_end/sdk/CorsIssue.js",
   "front_end/sdk/CrossOriginEmbedderPolicyIssue.js",
   "front_end/sdk/DOMDebuggerModel.js",
   "front_end/sdk/DOMModel.js",
diff --git a/front_end/i18n/locales/en-US.json b/front_end/i18n/locales/en-US.json
index 6397e53..840bf93 100644
--- a/front_end/i18n/locales/en-US.json
+++ b/front_end/i18n/locales/en-US.json
@@ -1847,6 +1847,39 @@
   "issues/AffectedTrustedWebActivityIssueDetailsView.ts | url": {
     "message": "Url"
   },
+  "issues/CorsIssueDetailsView.ts | blocked": {
+    "message": "blocked"
+  },
+  "issues/CorsIssueDetailsView.ts | initiatorAddressSpace": {
+    "message": "Initiator Address"
+  },
+  "issues/CorsIssueDetailsView.ts | initiatorContext": {
+    "message": "Initiator Context"
+  },
+  "issues/CorsIssueDetailsView.ts | insecure": {
+    "message": "insecure"
+  },
+  "issues/CorsIssueDetailsView.ts | item": {
+    "message": "item"
+  },
+  "issues/CorsIssueDetailsView.ts | items": {
+    "message": "items"
+  },
+  "issues/CorsIssueDetailsView.ts | request": {
+    "message": "Request"
+  },
+  "issues/CorsIssueDetailsView.ts | resourceAddressSpace": {
+    "message": "Resource Address"
+  },
+  "issues/CorsIssueDetailsView.ts | secure": {
+    "message": "secure"
+  },
+  "issues/CorsIssueDetailsView.ts | status": {
+    "message": "Status"
+  },
+  "issues/CorsIssueDetailsView.ts | warning": {
+    "message": "warning"
+  },
   "issues/CSPViolationsView.ts | filter": {
     "message": "Filter"
   },
diff --git a/front_end/i18n/locales/en-XL.json b/front_end/i18n/locales/en-XL.json
index 59907b5..133688d 100644
--- a/front_end/i18n/locales/en-XL.json
+++ b/front_end/i18n/locales/en-XL.json
@@ -1847,6 +1847,39 @@
   "issues/AffectedTrustedWebActivityIssueDetailsView.ts | url": {
     "message": "Ûŕl̂"
   },
+  "issues/CorsIssueDetailsView.ts | blocked": {
+    "message": "b̂ĺôćk̂éd̂"
+  },
+  "issues/CorsIssueDetailsView.ts | initiatorAddressSpace": {
+    "message": "Îńît́îát̂ór̂ Ád̂d́r̂éŝś"
+  },
+  "issues/CorsIssueDetailsView.ts | initiatorContext": {
+    "message": "Îńît́îát̂ór̂ Ćôńt̂éx̂t́"
+  },
+  "issues/CorsIssueDetailsView.ts | insecure": {
+    "message": "îńŝéĉúr̂é"
+  },
+  "issues/CorsIssueDetailsView.ts | item": {
+    "message": "ît́êḿ"
+  },
+  "issues/CorsIssueDetailsView.ts | items": {
+    "message": "ît́êḿŝ"
+  },
+  "issues/CorsIssueDetailsView.ts | request": {
+    "message": "R̂éq̂úêśt̂"
+  },
+  "issues/CorsIssueDetailsView.ts | resourceAddressSpace": {
+    "message": "R̂éŝóûŕĉé Âd́d̂ŕêśŝ"
+  },
+  "issues/CorsIssueDetailsView.ts | secure": {
+    "message": "ŝéĉúr̂é"
+  },
+  "issues/CorsIssueDetailsView.ts | status": {
+    "message": "Ŝt́ât́ûś"
+  },
+  "issues/CorsIssueDetailsView.ts | warning": {
+    "message": "ŵár̂ńîńĝ"
+  },
   "issues/CSPViolationsView.ts | filter": {
     "message": "F̂íl̂t́êŕ"
   },
diff --git a/front_end/issues/AffectedResourcesView.ts b/front_end/issues/AffectedResourcesView.ts
index 710e39e..c891f07 100644
--- a/front_end/issues/AffectedResourcesView.ts
+++ b/front_end/issues/AffectedResourcesView.ts
@@ -285,10 +285,16 @@
     header.appendChild(info);
   }
 
-  protected appendIssueDetailCell(element: HTMLElement, textContent: string): void {
+  protected createIssueDetailCell(textContent: string): HTMLTableDataCellElement {
     const cell = document.createElement('td');
     cell.textContent = textContent;
-    element.append(cell);
+    return cell;
+  }
+
+  protected appendIssueDetailCell(element: HTMLElement, textContent: string): HTMLTableDataCellElement {
+    const cell = this.createIssueDetailCell(textContent);
+    element.appendChild(cell);
+    return cell;
   }
 
   update(): void {
diff --git a/front_end/issues/BUILD.gn b/front_end/issues/BUILD.gn
index 4ecaa4e..a5985ff 100644
--- a/front_end/issues/BUILD.gn
+++ b/front_end/issues/BUILD.gn
@@ -15,6 +15,7 @@
     "CSPViolationsListView.ts",
     "CSPViolationsView.ts",
     "ComboBoxOfCheckBoxes.ts",
+    "CorsIssueDetailsView.ts",
     "IssueAggregator.js",
     "IssueRevealer.ts",
     "IssueView.js",
diff --git a/front_end/issues/CorsIssueDetailsView.ts b/front_end/issues/CorsIssueDetailsView.ts
new file mode 100644
index 0000000..62f56c1
--- /dev/null
+++ b/front_end/issues/CorsIssueDetailsView.ts
@@ -0,0 +1,127 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import * as i18n from '../i18n/i18n.js';
+import * as SDK from '../sdk/sdk.js';
+
+import {AffectedResourcesView} from './AffectedResourcesView.js';
+import {AggregatedIssue} from './IssueAggregator.js';
+import {IssueView} from './IssueView.js';
+
+export const UIStrings = {
+  /**
+  *@description Label for number of affected resources indication in issue view
+  */
+  item: 'item',
+  /**
+  *@description Label for number of affected resources indication in issue view
+  */
+  items: 'items',
+  /**
+  *@description Value for the status column in Shared Array Buffer issues
+  */
+  warning: 'warning',
+  /**
+  *@description The kind of resolution for a mixed content issue
+  */
+  blocked: 'blocked',
+  /**
+  *@description Text for the status of something
+  */
+  status: 'Status',
+  /**
+  *@description Text for the status of something
+  */
+  request: 'Request',
+  /**
+   *@description Text for the status of something
+   */
+  resourceAddressSpace: 'Resource Address',
+  /**
+   *@description Text for the status of something
+   */
+  initiatorAddressSpace: 'Initiator Address',
+  /**
+  *@description Text for the status of something
+  */
+  secure: 'secure',
+  /**
+  *@description Text for the status of something
+  */
+  insecure: 'insecure',
+  /**
+  *@description Text for the status of something
+  */
+  initiatorContext: 'Initiator Context',
+};
+const str_ = i18n.i18n.registerUIStrings('issues/CorsIssueDetailsView.ts', UIStrings);
+const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
+
+export class CorsIssueDetailsView extends AffectedResourcesView {
+  private issue: AggregatedIssue;
+
+  constructor(parentView: IssueView, issue: AggregatedIssue) {
+    super(parentView, {singular: i18nString(UIStrings.item), plural: i18nString(UIStrings.items)});
+    this.issue = issue;
+  }
+
+  private appendStatus(element: HTMLElement, isWarning: boolean): void {
+    const status = document.createElement('td');
+    if (isWarning) {
+      status.classList.add('affected-resource-report-only-status');
+      status.textContent = i18nString(UIStrings.warning);
+    } else {
+      status.classList.add('affected-resource-blocked-status');
+      status.textContent = i18nString(UIStrings.blocked);
+    }
+    element.appendChild(status);
+  }
+
+  private appendDetails(issues: Iterable<SDK.CorsIssue.CorsIssue>): void {
+    const header = document.createElement('tr');
+    this.appendColumnTitle(header, i18nString(UIStrings.request));
+    this.appendColumnTitle(header, i18nString(UIStrings.status));
+    this.appendColumnTitle(header, i18nString(UIStrings.resourceAddressSpace));
+    this.appendColumnTitle(header, i18nString(UIStrings.initiatorAddressSpace));
+    this.appendColumnTitle(header, i18nString(UIStrings.initiatorContext));
+
+    this.affectedResources.appendChild(header);
+    let count = 0;
+    for (const issue of issues) {
+      count++;
+      this.appendDetail(issue);
+    }
+    this.updateAffectedResourceCount(count);
+  }
+
+  private appendSecureContextCell(element: HTMLElement, isSecureContext: boolean|undefined): void {
+    if (isSecureContext === undefined) {
+      this.appendIssueDetailCell(element, '');
+      return;
+    }
+    this.appendIssueDetailCell(
+        element, isSecureContext ? i18nString(UIStrings.secure) : i18nString(UIStrings.insecure));
+  }
+
+
+  private appendDetail(issue: SDK.CorsIssue.CorsIssue): void {
+    const element = document.createElement('tr');
+    element.classList.add('affected-resource-directive');
+
+    const details = issue.details();
+    element.appendChild(this.createRequestCell(details.request));
+    this.appendStatus(element, details.isWarning);
+    this.appendIssueDetailCell(element, details.resourceIPAddressSpace ?? '');
+    this.appendIssueDetailCell(element, details.clientSecurityState?.initiatorIPAddressSpace ?? '');
+    this.appendSecureContextCell(element, details.clientSecurityState?.initiatorIsSecureContext);
+
+
+    this.affectedResources.appendChild(element);
+  }
+
+  update(): void {
+    this.clear();
+    this.appendDetails(this.issue.corsIssues());
+  }
+}
diff --git a/front_end/issues/IssueAggregator.js b/front_end/issues/IssueAggregator.js
index 9d30e6b..7c5a70c 100644
--- a/front_end/issues/IssueAggregator.js
+++ b/front_end/issues/IssueAggregator.js
@@ -38,6 +38,8 @@
     this._aggregatedIssuesCount = 0;
     /** @type {Set<SDK.SharedArrayBufferIssue.SharedArrayBufferIssue>} */
     this._sharedArrayBufferIssues = new Set();
+    /** @type {Set<SDK.CorsIssue.CorsIssue>} */
+    this._corsIssues = new Set();
   }
 
   /**
@@ -95,6 +97,12 @@
     return this._trustedWebActivityIssues;
   }
 
+  /**
+   * @return {Iterable<SDK.CorsIssue.CorsIssue>}
+   */
+  corsIssues() {
+    return this._corsIssues;
+  }
 
   /**
    * @returns {!Iterable<!SDK.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue>}
@@ -208,6 +216,9 @@
     if (issue instanceof SDK.LowTextContrastIssue.LowTextContrastIssue) {
       this._lowContrastIssues.add(issue);
     }
+    if (issue instanceof SDK.CorsIssue.CorsIssue) {
+      this._corsIssues.add(issue);
+    }
   }
 }
 
diff --git a/front_end/issues/IssueView.js b/front_end/issues/IssueView.js
index 534626e..2446f74 100644
--- a/front_end/issues/IssueView.js
+++ b/front_end/issues/IssueView.js
@@ -18,6 +18,7 @@
 import {AffectedItem, AffectedResourcesView, extractShortPath} from './AffectedResourcesView.js';
 import {AffectedSharedArrayBufferIssueDetailsView} from './AffectedSharedArrayBufferIssueDetailsView.js';
 import {AffectedTrustedWebActivityIssueDetailsView} from './AffectedTrustedWebActivityIssueDetailsView.js';
+import {CorsIssueDetailsView} from './CorsIssueDetailsView.js';
 import {AggregatedIssue} from './IssueAggregator.js';            // eslint-disable-line no-unused-vars
 import {IssueDescription} from './MarkdownIssueDescription.js';  // eslint-disable-line no-unused-vars
 
@@ -829,6 +830,7 @@
       new AffectedSharedArrayBufferIssueDetailsView(this, this._issue),
       new AffectedElementsWithLowContrastView(this, this._issue),
       new AffectedTrustedWebActivityIssueDetailsView(this, this._issue),
+      new CorsIssueDetailsView(this, this._issue),
     ];
 
     this._aggregatedIssuesCount = null;
diff --git a/front_end/issues/descriptions/corsInsecurePrivateNetwork.md b/front_end/issues/descriptions/corsInsecurePrivateNetwork.md
new file mode 100644
index 0000000..db4094b
--- /dev/null
+++ b/front_end/issues/descriptions/corsInsecurePrivateNetwork.md
@@ -0,0 +1,10 @@
+# Ensure private network requests are made from secure contexts
+
+A site requested a resource from a network that it could only access because of its users' privileged network position.
+These requests expose devices and servers to the internet, increasing the risk of a cross-site request forgery (CSRF) attack, and/or information leakage.
+
+To mitigate these risks, Chrome deprecates requests to non-public subresources when initiated from non-secure contexts, and will start blocking them in Chrome 92 (July 2021).
+
+To fix this issue, migrate the website that needs to access local resources to HTTPS. If the target resource is not served on localhost, it must also be served on HTTPS to avoid mixed-content issues.
+
+Administrators can make use of the `InsecurePrivateNetworkRequestsAllowed` and `InsecurePrivateNetworkRequestsAllowedForUrls` enterprise policies to temporarily disable this restriction on all or certain websites.
diff --git a/front_end/issues/module.json b/front_end/issues/module.json
index bff5af2..7e9f040 100644
--- a/front_end/issues/module.json
+++ b/front_end/issues/module.json
@@ -23,6 +23,7 @@
     "descriptions/LowTextContrast.md",
     "descriptions/mixedContent.md",
     "descriptions/sharedArrayBuffer.md",
+    "descriptions/corsInsecurePrivateNetwork.md",
     "descriptions/SameSiteExcludeContextDowngradeRead.md",
     "descriptions/SameSiteExcludeContextDowngradeSet.md",
     "descriptions/SameSiteExcludeNavigationContextDowngrade.md",
diff --git a/front_end/sdk/BUILD.gn b/front_end/sdk/BUILD.gn
index e7377b0..dc8f969 100644
--- a/front_end/sdk/BUILD.gn
+++ b/front_end/sdk/BUILD.gn
@@ -29,6 +29,7 @@
     "Cookie.js",
     "CookieModel.js",
     "CookieParser.js",
+    "CorsIssue.ts",
     "CrossOriginEmbedderPolicyIssue.js",
     "DOMDebuggerModel.js",
     "DOMModel.js",
diff --git a/front_end/sdk/CorsIssue.ts b/front_end/sdk/CorsIssue.ts
new file mode 100644
index 0000000..1879aa6
--- /dev/null
+++ b/front_end/sdk/CorsIssue.ts
@@ -0,0 +1,41 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ls} from '../common/common.js';  // eslint-disable-line rulesdir/es_modules_import
+
+import {Issue, IssueCategory, IssueKind, MarkdownIssueDescription} from './Issue.js';  // eslint-disable-line no-unused-vars
+import {IssuesModel} from './IssuesModel.js';  // eslint-disable-line no-unused-vars
+
+export class CorsIssue extends Issue {
+  private issueDetails: Protocol.Audits.CorsIssueDetails;
+
+  constructor(issueDetails: Protocol.Audits.CorsIssueDetails, issuesModel: IssuesModel) {
+    super(Protocol.Audits.InspectorIssueCode.CorsIssue, issuesModel);
+    this.issueDetails = issueDetails;
+  }
+
+  getCategory(): IssueCategory {
+    return IssueCategory.Cors;
+  }
+
+  details(): Protocol.Audits.CorsIssueDetails {
+    return this.issueDetails;
+  }
+
+  getDescription(): MarkdownIssueDescription {
+    return {
+      file: 'issues/descriptions/corsInsecurePrivateNetwork.md',
+      substitutions: undefined,
+      issueKind: IssueKind.BreakingChange,
+      links: [{
+        link: 'https://web.dev/cors-rfc1918-feedback/',
+        linkTitle: ls`CORS for private networks (RFC1918)`,
+      }],
+    };
+  }
+
+  primaryKey(): string {
+    return JSON.stringify(this.issueDetails);
+  }
+}
diff --git a/front_end/sdk/Issue.js b/front_end/sdk/Issue.js
index 16a0896..43ff592 100644
--- a/front_end/sdk/Issue.js
+++ b/front_end/sdk/Issue.js
@@ -16,6 +16,7 @@
   ContentSecurityPolicy: Symbol('ContentSecurityPolicy'),
   TrustedWebActivity: Symbol('TrustedWebActivity'),
   LowTextContrast: Symbol('LowTextContrast'),
+  Cors: Symbol('Cors'),
   Other: Symbol('Other')
 };
 
diff --git a/front_end/sdk/IssuesModel.js b/front_end/sdk/IssuesModel.js
index 4f6b7f1..9c57dd9 100644
--- a/front_end/sdk/IssuesModel.js
+++ b/front_end/sdk/IssuesModel.js
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import {ContentSecurityPolicyIssue} from './ContentSecurityPolicyIssue.js';
+import {CorsIssue} from './CorsIssue.js';
 import {CrossOriginEmbedderPolicyIssue, isCrossOriginEmbedderPolicyIssue} from './CrossOriginEmbedderPolicyIssue.js';
 import {HeavyAdIssue} from './HeavyAdIssue.js';
 import {Issue} from './Issue.js';  // eslint-disable-line no-unused-vars
@@ -217,6 +218,20 @@
 }
 
 /**
+ * @param {!IssuesModel} issuesModel
+ * @param {!Protocol.Audits.InspectorIssueDetails} inspectorDetails
+ * @return {!Array<!Issue>}
+ */
+function createIssuesForCorsIssue(issuesModel, inspectorDetails) {
+  const corsIssueDetails = inspectorDetails.corsIssueDetails;
+  if (!corsIssueDetails) {
+    console.warn('Cors issue without details received.');
+    return [];
+  }
+  return [new CorsIssue(corsIssueDetails, issuesModel)];
+}
+
+/**
  * @type {!Map<!Protocol.Audits.InspectorIssueCode, function(!IssuesModel, !Protocol.Audits.InspectorIssueDetails):!Array<!Issue>>}
  */
 const issueCodeHandlers = new Map([
@@ -228,6 +243,7 @@
   [Protocol.Audits.InspectorIssueCode.SharedArrayBufferIssue, createIssuesForSharedArrayBufferIssue],
   [Protocol.Audits.InspectorIssueCode.TrustedWebActivityIssue, createIssuesForTrustedWebActivityIssue],
   [Protocol.Audits.InspectorIssueCode.LowTextContrastIssue, createIssuesForLowTextContrastIssue],
+  [Protocol.Audits.InspectorIssueCode.CorsIssue, createIssuesForCorsIssue],
 ]);
 
 /** @enum {symbol} */
diff --git a/front_end/sdk/sdk.js b/front_end/sdk/sdk.js
index 4cd0e35..8d11997 100644
--- a/front_end/sdk/sdk.js
+++ b/front_end/sdk/sdk.js
@@ -19,6 +19,7 @@
 import * as Cookie from './Cookie.js';
 import * as CookieModel from './CookieModel.js';
 import * as CookieParser from './CookieParser.js';
+import * as CorsIssue from './CorsIssue.js';
 import * as CPUProfileDataModel from './CPUProfileDataModel.js';
 import * as CPUProfilerModel from './CPUProfilerModel.js';
 import * as CSSFontFace from './CSSFontFace.js';
@@ -88,6 +89,7 @@
   Cookie,
   CookieModel,
   CookieParser,
+  CorsIssue,
   CPUProfileDataModel,
   CPUProfilerModel,
   CSSFontFace,
diff --git a/front_end/sdk/sdk_strings.grdp b/front_end/sdk/sdk_strings.grdp
index 181cf10..e2d1b1c 100644
--- a/front_end/sdk/sdk_strings.grdp
+++ b/front_end/sdk/sdk_strings.grdp
@@ -3,6 +3,9 @@
   <message name="IDS_DEVTOOLS_0203e27fe44545283879b18e7084391b" desc="Error message when failing to load a script source text">
     Unable to fetch script source.
   </message>
+  <message name="IDS_DEVTOOLS_2eb412338beb67c81288a2f3e2da91e9" desc="Label for the link for CORS private network issues">
+    CORS for private networks (RFC1918)
+  </message>
   <message name="IDS_DEVTOOLS_46334eeb893715e9aefe809c08ce92b9" desc="Label for the link for Trusted Web Activity issue">
     Changes to quality criteria for PWAs using Trusted Web Activity
   </message>
diff --git a/test/e2e/issues/BUILD.gn b/test/e2e/issues/BUILD.gn
index 2f70549..9d13547 100644
--- a/test/e2e/issues/BUILD.gn
+++ b/test/e2e/issues/BUILD.gn
@@ -6,6 +6,7 @@
 
 node_ts_library("issues") {
   sources = [
+    "cors-private-network-issues_test.ts",
     "csp-violations-tab_test.ts",
     "group-by-categories_test.ts",
     "heavy-ad-issues_test.ts",
diff --git a/test/e2e/issues/cors-private-network-issues_test.ts b/test/e2e/issues/cors-private-network-issues_test.ts
new file mode 100644
index 0000000..fc4c68c
--- /dev/null
+++ b/test/e2e/issues/cors-private-network-issues_test.ts
@@ -0,0 +1,88 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+
+import {assertNotNull, getBrowserAndPages, goToResource} from '../../shared/helper.js';
+import {describe, it} from '../../shared/mocha-extensions.js';
+import {expandIssue, expandResourceSection, extractTableFromResourceSection, getIssueByTitle, getResourcesElement, navigateToIssuesTab} from '../helpers/issues-helpers.js';
+
+describe('Cors Private Network issue', async () => {
+  beforeEach(async () => {
+    await goToResource('issues/sab-issue.html');
+  });
+
+  it('should display correct information', async () => {
+    await navigateToIssuesTab();
+    const {frontend} = getBrowserAndPages();
+    frontend.evaluate(() => {
+      const issue = {
+        code: 'CorsIssue',
+        details: {
+          corsIssueDetails: {
+            clientSecurityState: {
+              initiatorIsSecureContext: false,
+              initiatorIPAddressSpace: 'Public',
+              privateNetworkRequestPolicy: 'WarnFromInsecureToMorePrivate',
+            },
+            corsErrorStatus: {corsError: 'InsecurePrivateNetwork', failedParameter: ''},
+            isWarning: true,
+            request: {requestId: 'request-1', url: 'http://localhost/'},
+            resourceIPAddressSpace: 'Local',
+          },
+        },
+      };
+      // @ts-ignore
+      window.addIssueForTest(issue);
+      const issue2 = {
+        code: 'CorsIssue',
+        details: {
+          corsIssueDetails: {
+            clientSecurityState: {
+              initiatorIsSecureContext: false,
+              initiatorIPAddressSpace: 'Unknown',
+              privateNetworkRequestPolicy: 'WarnFromInsecureToMorePrivate',
+            },
+            corsErrorStatus: {corsError: 'InsecurePrivateNetwork', failedParameter: ''},
+            isWarning: true,
+            request: {requestId: 'request-1', url: 'http://example.com/'},
+            resourceIPAddressSpace: 'Local',
+          },
+        },
+      };
+      // @ts-ignore
+      window.addIssueForTest(issue2);
+    });
+
+    await expandIssue();
+    const issueElement = await getIssueByTitle('Ensure private network requests are made from secure contexts');
+    assertNotNull(issueElement);
+    const section = await getResourcesElement('2 items', issueElement);
+    await expandResourceSection(section);
+    const table = await extractTableFromResourceSection(section.content);
+    assertNotNull(table);
+    assert.strictEqual(table.length, 3);
+    assert.deepEqual(table[0], [
+      'Request',
+      'Status',
+      'Resource Address',
+      'Initiator Address',
+      'Initiator Context',
+    ]);
+    assert.deepEqual(table[1], [
+      'localhost/',
+      'warning',
+      'Local',
+      'Public',
+      'insecure',
+    ]);
+    assert.deepEqual(table[2], [
+      'example.com/',
+      'warning',
+      'Local',
+      'Unknown',
+      'insecure',
+    ]);
+  });
+});