OSSCA Chromium

기여 목록 / 8451582

[security] Migrate SecurityOriginView note section to Lit

DevToolsrefactordevtools/devtools-frontendMerged

SecurityOriginView의 note section을 Lit으로 전환하고, 표시 조건과 캐시 안내 테스트를 추가했습니다. Certificate Transparency section(CT section)이 표시되지 않을 때 일반 안내 문구도 누락되던 문제를 함께 수정했습니다.

문제 설명

기존 코드는 SCT가 없고 CT compliance가 Unknown이면 생성자에서 바로 반환해, 뒤에 있는 note section도 생성하지 않았습니다. security details가 있어도 첫 번째로 검사한 응답의 정보라는 안내와 캐시 안내가 누락될 수 있었습니다.

해결 내용

  • renderNoteSection()에서 캐시 안내와 첫 번째로 검사한 응답에 대한 안내를 Lit 템플릿으로 렌더링하도록 했습니다.
  • CT section을 생략할 때 생성자에서 반환하던 코드를 바꿔, 해당 영역만 조건부로 생성하도록 했습니다. security details가 있으면 CT section 표시 여부와 관계없이 일반 안내 문구를 표시합니다.
  • loadedFromCache가 참이면 캐시 안내를 먼저 표시하고, security details가 없으면 note section을 생성하지 않는 조건은 유지했습니다.
  • 기존 CT 테스트에서 note section이 없다는 검증을 제거하고, note section의 단위 테스트 7개를 추가했습니다.

전체 코드 diff는 다음과 같습니다.

diff --git a/front_end/panels/security/SecurityPanel.test.ts b/front_end/panels/security/SecurityPanel.test.ts
index cbdfe4c..a29ad99 100644
--- a/front_end/panels/security/SecurityPanel.test.ts
+++ b/front_end/panels/security/SecurityPanel.test.ts
@@ -388,7 +388,6 @@
           }));

       assert.notExists(view.element.querySelector('.certificate-transparency-section'));
-      assert.notExists(view.element.querySelector('.origin-view-notes'));
     });

     it('renders without a note when the SCT list is not empty and compliance is unknown', () => {
@@ -473,6 +472,85 @@
     });
   });

+  describe('note section', () => {
+    function getNoteTexts(view: Security.SecurityPanel.SecurityOriginView): string[] {
+      const notes = querySelectorErrorOnMissing(view.element, '.origin-view-notes');
+      return Array.from(notes.children, note => note.textContent ?? '');
+    }
+
+    it('renders when security details are available', () => {
+      const originState = createOriginState();
+      originState.securityDetails = {
+        protocol: 'TLS 1.3',
+        keyExchange: '',
+        cipher: 'AES_128_GCM',
+        certificateId: 0 as Protocol.Security.CertificateId,
+        subjectName: 'example.com',
+        sanList: [],
+        issuer: 'Test CA',
+        validFrom: 0,
+        validTo: 1,
+        signedCertificateTimestampList: [],
+        certificateTransparencyCompliance: Protocol.Network.CertificateTransparencyCompliance.Unknown,
+        encryptedClientHello: false,
+      };
+      const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+      assert.deepEqual(getNoteTexts(view), ['The security details above are from the first inspected response.']);
+    });
+
+    it('does not render when security details are null', () => {
+      const originState = createOriginState();
+      originState.securityDetails = null;
+      const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+      assert.notExists(view.element.querySelector('.origin-view-notes'));
+    });
+
+    it('renders with the cache note when loadedFromCache is true', () => {
+      const originState = createOriginState();
+      originState.loadedFromCache = true;
+      const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+      assert.deepEqual(getNoteTexts(view), [
+        'This response was loaded from cache. Some security details might be missing.',
+        'The security details above are from the first inspected response.',
+      ]);
+    });
+
+    it('renders when the SCT list is not empty', () => {
+      const originState = createOriginState({
+        signedCertificateTimestampList: [{
+          logDescription: 'Test log',
+          logId: '00',
+          status: 'Verified',
+          origin: 'Embedded in certificate',
+          timestamp: 0,
+          hashAlgorithm: 'SHA-256',
+          signatureAlgorithm: 'ECDSA',
+          signatureData: '00',
+        }],
+      });
+      const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+      assert.exists(view.element.querySelector('.origin-view-notes'));
+    });
+
+    const compliances = [
+      Protocol.Network.CertificateTransparencyCompliance.Unknown,
+      Protocol.Network.CertificateTransparencyCompliance.Compliant,
+      Protocol.Network.CertificateTransparencyCompliance.NotCompliant,
+    ];
+    for (const compliance of compliances) {
+      it(`renders when certificate transparency compliance is ${compliance}`, () => {
+        const originState = createOriginState({certificateTransparencyCompliance: compliance});
+        const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+        assert.exists(view.element.querySelector('.origin-view-notes'));
+      });
+    }
+  });
+
   it('renders an empty SAN', () => {
     const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, createOriginState());

diff --git a/front_end/panels/security/SecurityPanel.ts b/front_end/panels/security/SecurityPanel.ts
index 420c4ef..8f2833b 100644
--- a/front_end/panels/security/SecurityPanel.ts
+++ b/front_end/panels/security/SecurityPanel.ts
@@ -1562,6 +1562,14 @@
   // clang-format on
 }

+function renderNoteSection(loadedFromCache: boolean): TemplateResult {
+  // clang-format off
+  return html`
+    ${loadedFromCache ? html`<div>${i18nString(UIStrings.thisResponseWasLoadedFromCache)}</div>` : nothing}
+    <div>${i18nString(UIStrings.theSecurityDetailsAboveAreFrom)}</div>`;
+  // clang-format on
+}
+
 export class SecurityOriginView extends UI.Widget.VBox {
   readonly #origin: Platform.DevToolsPath.UrlString;
   readonly #titleSection: HTMLElement;
@@ -1586,16 +1594,13 @@

       const sctListLength = originState.securityDetails.signedCertificateTimestampList.length;
       const ctCompliance = originState.securityDetails.certificateTransparencyCompliance;
-      if (!sctListLength && ctCompliance === Protocol.Network.CertificateTransparencyCompliance.Unknown) {
-        return;
+      if (sctListLength || ctCompliance !== Protocol.Network.CertificateTransparencyCompliance.Unknown) {
+        this.#createCertificateTransparencySection(originState.securityDetails);
       }
-      this.#createCertificateTransparencySection(originState.securityDetails);

       const noteSection = this.element.createChild('div', 'origin-view-section origin-view-notes');
-      if (originState.loadedFromCache) {
-        noteSection.createChild('div').textContent = i18nString(UIStrings.thisResponseWasLoadedFromCache);
-      }
-      noteSection.createChild('div').textContent = i18nString(UIStrings.theSecurityDetailsAboveAreFrom);
+      // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
+      render(renderNoteSection(originState.loadedFromCache), noteSection);
     } else if (originState.securityState === Protocol.Security.SecurityState.Secure) {
       // If the security state is secure but there are no security details,
       // this means that the origin is a non-cryptographic secure origin, e.g.

테스트 방법

SecurityPanel.test.ts에 다음 내용을 검증하는 단위 테스트 7개를 추가했습니다.

  • security details가 있으면 일반 안내 문구를 표시하고, null이면 영역을 생략하는지 확인
  • 캐시 응답이면 캐시 안내와 일반 안내를 순서대로 표시하는지 확인
  • SCT 목록에 항목이 있으면 note section을 표시하는지 확인
  • CT compliance가 Unknown, Compliant, NotCompliant인 세 경우 모두 note section을 표시하는지 확인

Chromium 웹 테스트 security-details-updated-with-security-state.js의 기대값은 CL 8458524에서 임시 조정했습니다. 최종 patchset의 CQ 실행이 통과했으며, 2026년 9월 28일(한국 시간) DevTools main에 병합되었습니다.

배운 점

  • 한 영역을 생략하기 위한 조기 반환이 뒤에 있는 다른 영역까지 생략하는지 확인해야 합니다.
  • 독립적으로 표시해야 하는 영역은 각각의 조건으로 렌더링하고, 표시 조건의 조합을 테스트로 검증해야 합니다.

참고 자료