diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3c93c4e..2e02f19 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/init@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +68,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/autobuild@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -81,6 +81,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/analyze@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 35b06ab..adf20ed 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2ffad79..a4673a1 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 with: results_file: results.sarif results_format: sarif @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 + uses: github/codeql-action/upload-sarif@d90b8d79de6dc1f58e83a1499aa58d6c93dc28de # v2.22.2 with: sarif_file: results.sarif diff --git a/README.md b/README.md index e08c0db..8e425ce 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ question(message: string, options?: PromptOptions): Promise ``` Simple prompt, similar to `rl.question()` with an improved UI. +Use `options.secure` if you need to hide both input and answer. Use `options.validators` to handle user input. **Example** @@ -156,6 +157,7 @@ export interface Validator { export interface QuestionOptions extends SharedOptions { defaultValue?: string; validators?: Validator[]; + secure?: boolean; } export interface Choice { diff --git a/index.d.ts b/index.d.ts index 2d42e82..92eab7c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -15,6 +15,7 @@ export interface Validator { export interface QuestionOptions extends SharedOptions { defaultValue?: string; validators?: Validator[]; + secure?: boolean; } export interface Choice { diff --git a/package.json b/package.json index aed00b1..7fe9347 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "type": "module", "devDependencies": { "@nodesecure/eslint-config": "^1.8.0", - "@types/node": "^20.8.2", + "@types/node": "^20.8.5", "c8": "^8.0.1", - "eslint": "^8.50.0", - "esmock": "^2.5.1" + "eslint": "^8.51.0", + "esmock": "^2.5.2" }, "dependencies": { "is-unicode-supported": "^1.3.0", diff --git a/src/abstract-prompt.js b/src/abstract-prompt.js index cf2e847..9f39af3 100644 --- a/src/abstract-prompt.js +++ b/src/abstract-prompt.js @@ -1,6 +1,7 @@ // Import Node.js Dependencies import { EOL } from "node:os"; import { createInterface } from "node:readline"; +import { Writable } from "node:stream"; // Import Third-party Dependencies import stripAnsi from "strip-ansi"; @@ -23,11 +24,23 @@ export class AbstractPrompt { this.message = message; this.history = []; this.agent = PromptAgent.agent(); + this.mute = false; if (this.stdout.isTTY) { this.stdin.setRawMode(true); } - this.rl = createInterface({ input, output }); + this.rl = createInterface({ + input, + output: new Writable({ + write: (chunk, encoding, callback) => { + if (!this.mute) { + this.stdout.write(chunk, encoding); + } + callback(); + } + }), + terminal: true + }); } write(data) { diff --git a/src/question-prompt.js b/src/question-prompt.js index 656ead2..d800ace 100644 --- a/src/question-prompt.js +++ b/src/question-prompt.js @@ -10,13 +10,15 @@ import { SYMBOLS } from "./constants.js"; export class QuestionPrompt extends AbstractPrompt { #validators; + #secure; constructor(message, options = {}) { const { stdin = process.stdin, stdout = process.stdout, defaultValue, - validators = [] + validators = [], + secure = false } = options; super(message, stdin, stdout); @@ -28,6 +30,7 @@ export class QuestionPrompt extends AbstractPrompt { this.defaultValue = defaultValue; this.tip = this.defaultValue ? ` (${this.defaultValue})` : ""; this.#validators = validators; + this.#secure = Boolean(secure); this.questionSuffixError = ""; } @@ -37,9 +40,11 @@ export class QuestionPrompt extends AbstractPrompt { this.rl.question(questionQuery, (answer) => { this.history.push(questionQuery + answer); + this.mute = false; resolve(answer); }); + this.mute = this.#secure; }); } @@ -54,8 +59,7 @@ export class QuestionPrompt extends AbstractPrompt { #writeAnswer() { const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross; - const answer = kleur.yellow(this.answer ?? ""); - + const answer = kleur.yellow(this.#secure ? "CONFIDENTIAL" : this.answer ?? ""); this.write(`${prefix} ${kleur.bold(this.message)} ${SYMBOLS.Pointer} ${answer}${EOL}`); } diff --git a/test/helpers/mock-process.js b/test/helpers/mock-process.js index cdd48a7..98e3292 100755 --- a/test/helpers/mock-process.js +++ b/test/helpers/mock-process.js @@ -26,7 +26,8 @@ export function mockProcess(inputs, writeCb) { pause: () => true, paused: () => false, resume: () => true, - removeListener: () => true + removeListener: () => true, + listenerCount: () => true }; return { stdout, stdin }; diff --git a/test/helpers/testing-prompt.js b/test/helpers/testing-prompt.js index f6db49e..7a9cdf2 100755 --- a/test/helpers/testing-prompt.js +++ b/test/helpers/testing-prompt.js @@ -7,7 +7,7 @@ import { mockProcess } from "./mock-process.js"; export class TestingPrompt { static async QuestionPrompt(message, options) { - const { input, onStdoutWrite, defaultValue, validators } = options; + const { input, onStdoutWrite, defaultValue, validators, secure } = options; const inputs = Array.isArray(input) ? input : [input]; const { QuestionPrompt } = await esmock("../../src/question-prompt", { }, { @@ -25,7 +25,7 @@ export class TestingPrompt { }); const { stdin, stdout } = mockProcess([], (data) => onStdoutWrite(data)); - return new QuestionPrompt(message, { stdin, stdout, defaultValue, validators }); + return new QuestionPrompt(message, { stdin, stdout, defaultValue, validators, secure }); } static async SelectPrompt(message, options) { diff --git a/test/question-prompt.test.js b/test/question-prompt.test.js index e2778e2..468f9b1 100755 --- a/test/question-prompt.test.js +++ b/test/question-prompt.test.js @@ -108,4 +108,33 @@ describe("QuestionPrompt", () => { message: "defaultValue must be a string" }); }); + + it("should not display answer when prompt is secure", async() => { + const logs = []; + const questionPrompt = await TestingPrompt.QuestionPrompt("What's your name?", { + input: ["John Deeoe"], + secure: true, + onStdoutWrite: (log) => logs.push(log) + }); + const input = await questionPrompt.question(); + + assert.equal(input, "John Deeoe"); + assert.deepStrictEqual(logs, [ + "? What's your name?", + "✔ What's your name? â€ē CONFIDENTIAL" + ]); + }); + + it("should not display answer when prompt is secure and using PromptAgent", async() => { + const logs = []; + const { stdin, stdout } = mockProcess([], (text) => logs.push(text)); + kPromptAgent.nextAnswer("John Doe"); + + const input = await question("What's your name?", { secure: true, stdin, stdout }); + + assert.equal(input, "John Doe"); + assert.deepStrictEqual(logs, [ + "✔ What's your name? â€ē CONFIDENTIAL" + ]); + }); });