From 9608a89ac28202d99e5a43e5eca09654bd450d7e Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Mon, 25 Mar 2024 15:02:55 +0200 Subject: [PATCH] feat: Implement html script linter --- src/detectors/transpilers/html/transpiler.ts | 23 ++++++------ src/linter/html/linter.ts | 37 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/detectors/transpilers/html/transpiler.ts b/src/detectors/transpilers/html/transpiler.ts index b2c48867a..c826b6e32 100644 --- a/src/detectors/transpilers/html/transpiler.ts +++ b/src/detectors/transpilers/html/transpiler.ts @@ -1,8 +1,8 @@ -import type { ReadStream } from "node:fs"; -import { Detail, SaxEventType, SAXParser, Tag as SaxTag } from "sax-wasm"; -import { finished } from "node:stream/promises"; +import type {ReadStream} from "node:fs"; +import {Detail, SaxEventType, SAXParser, Tag as SaxTag} from "sax-wasm"; +import {finished} from "node:stream/promises"; import fs from "node:fs/promises"; -import { createRequire } from "node:module"; +import {createRequire} from "node:module"; const require = createRequire(import.meta.url); let saxWasmBuffer: Buffer; @@ -15,7 +15,7 @@ async function initSaxWasm() { return saxWasmBuffer; } -export async function parseHtml(contentStream: ReadStream, parseHandler: (type: SaxEventType, tag: Detail) => void) { +async function parseHtml(contentStream: ReadStream, parseHandler: (type: SaxEventType, tag: Detail) => void) { const options = { highWaterMark: 32 * 1024 }; // 32k chunks const saxWasmBuffer = await initSaxWasm(); const saxParser = new SAXParser(SaxEventType.OpenTag | SaxEventType.CloseTag, options); @@ -46,13 +46,14 @@ export async function parseHtml(contentStream: ReadStream, parseHandler: (type: } export async function extractScriptTags(contentStream: ReadStream) { + const scriptTags: SaxTag[] = []; await parseHtml(contentStream, (event, tag) => { - if (tag instanceof SaxTag) { - if (event === SaxEventType.OpenTag) { - console.log(tag.value); - } else if (event === SaxEventType.CloseTag) { - console.log(tag.value); - } + if (tag instanceof SaxTag && + event === SaxEventType.CloseTag && + tag.value === "script") { + scriptTags.push(tag); } }); + + return scriptTags; } \ No newline at end of file diff --git a/src/linter/html/linter.ts b/src/linter/html/linter.ts index 72402c768..659e0dad6 100644 --- a/src/linter/html/linter.ts +++ b/src/linter/html/linter.ts @@ -1,4 +1,7 @@ -import { taskStart } from "../../detectors/util/perf.js"; +import {taskStart} from "../../detectors/util/perf.js"; +import {extractScriptTags} from "../../detectors/transpilers/html/transpiler.js"; +import {LintMessageSeverity} from "../../detectors/AbstractDetector.js"; +import Reporter from "../å../detectors/Reporter.js"; import type { TranspileResult } from "../../detectors/transpilers/AbstractTranspiler.js"; import type { ReadStream } from "node:fs"; @@ -6,7 +9,37 @@ import type { ReadStream } from "node:fs"; export async function lintHtml(resourceName: string, contentStream: ReadStream): Promise { const taskLintEnd = taskStart("Static lint", resourceName); + const report = new Reporter("", resourceName); + + const scriptTags = await extractScriptTags(contentStream); + const jsScriptTags = scriptTags.filter((tag) => tag.attributes.every((attr) => { + // The "type" attribute of the script tag should be + // 1. not set (default), + // 2. an empty string, + // 3. or a JavaScript MIME type (text/javascript) + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type + return attr.name.value !== "type" || + (attr.name.value === "type" && + (attr.value.value === "" || attr.value.value === "text/javascript")); + })); + + + jsScriptTags.forEach((tag) => { + const scriptContent = tag.textNodes?.map((tNode) => tNode.value).join("").trim(); + + if (scriptContent) { + report.addMessage({ + // node: `/sap.ui5/dependencies/libs/${libKey}`, + severity: LintMessageSeverity.Error, + ruleId: "ui5-linter-csp-compliance", + message: `Use of inline javascript`, + messageDetails: "In order to avoid CSP errors, avoid usage of inline javascript", + }); + } + }); + taskLintEnd(); - return { messages: [], source: "", map: "" }; + const {messages} = report.getReport(); + return {messages, source: "", map: ""}; }