@ojas-sta/qalify-plus 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ require('dotenv').config();
3
+ const path = require('path');
4
+ const BrowserLayer = require('./browser');
5
+ const OCRLayer = require('./ocr');
6
+ const AILayer = require('./ai');
7
+ const AnalyzerLayer = require('./analyzer');
8
+
9
+ async function main() {
10
+ const args = process.argv.slice(2);
11
+ const stressMode = args.includes('--stress');
12
+
13
+ console.log("=========================================");
14
+ console.log(" QUIZ AUTOMATION SIMULATOR STARTING ");
15
+ console.log("=========================================");
16
+ if (stressMode) {
17
+ console.log("[!] STRESS MODE ACTIVATED - Simulating Obfuscated DOM");
18
+ }
19
+
20
+ if (!process.env.GEMINI_API_KEY) {
21
+ console.warn("\n[WARNING] GEMINI_API_KEY environment variable is not set.");
22
+ console.warn("The AI Reasoning layer will fail. Please set it before running for full functionality.\n");
23
+ }
24
+
25
+ const browserLayer = new BrowserLayer();
26
+ const ocrLayer = new OCRLayer();
27
+ const aiLayer = new AILayer();
28
+ const analyzerLayer = new AnalyzerLayer();
29
+
30
+ try {
31
+ await browserLayer.init();
32
+
33
+ // Expose chat function to browser overlay
34
+ await browserLayer.setupChatCallback(async (msg) => {
35
+ return await aiLayer.chat(msg);
36
+ });
37
+
38
+ const targetUrl = `file://${path.resolve(__dirname, '../test/quiz.html')}`;
39
+ await browserLayer.navigateToQuiz(targetUrl, stressMode);
40
+
41
+ let isRunning = true;
42
+ let questionNumber = 1;
43
+
44
+ while (isRunning) {
45
+ console.log(`\n--- Processing Question ${questionNumber} ---`);
46
+ let extractedData = null;
47
+ let extractionMethod = 'None';
48
+
49
+ // 1. Attempt DOM Extraction
50
+ extractedData = await browserLayer.extractFromDOM();
51
+
52
+ if (extractedData) {
53
+ console.log("[SUCCESS] Extracted via DOM.");
54
+ extractionMethod = 'DOM';
55
+ } else {
56
+ console.log("[INFO] DOM Extraction failed or yielded no usable results. Falling back to OCR.");
57
+
58
+ // 2. Fallback to OCR
59
+ const screenshotPath = path.resolve(__dirname, '../screenshot.png');
60
+ await browserLayer.captureScreenshot(screenshotPath);
61
+
62
+ const processedImagePath = path.resolve(__dirname, '../processed_screenshot.png');
63
+ const finalImagePath = await ocrLayer.preprocessImage(screenshotPath, processedImagePath);
64
+
65
+ extractedData = await ocrLayer.extractText(finalImagePath);
66
+
67
+ if (extractedData) {
68
+ console.log("[SUCCESS] Extracted via OCR.");
69
+ extractionMethod = 'OCR';
70
+ } else {
71
+ console.log("[FAILURE] OCR Extraction also failed.");
72
+ }
73
+ }
74
+
75
+ let aiOutput = null;
76
+ // 3. AI Reasoning Layer
77
+ if (extractedData && extractedData.question) {
78
+ aiOutput = await aiLayer.determineAnswer(extractedData.question, extractedData.options);
79
+ if (aiOutput) {
80
+ console.log(`[AI] Selected Option: ${aiOutput.selectedOption} (${aiOutput.confidenceScore}% confidence)`);
81
+
82
+ // VISUAL LAYER: Show reasoning and click
83
+ await browserLayer.showAIReasoning(aiOutput);
84
+ await browserLayer.clickOption(aiOutput.selectedOption);
85
+ }
86
+ }
87
+
88
+ // 4. Vulnerability Analysis
89
+ analyzerLayer.evaluate(extractedData, extractionMethod, aiOutput, stressMode);
90
+
91
+ // Get the current text before waiting
92
+ const currentContainerText = await browserLayer.getCurrentContainerText();
93
+
94
+ console.log("\n[INFO] AI has answered. You can now chat in the overlay, or click 'Next Question' in the browser to continue...");
95
+
96
+ // Wait for the container text to change
97
+ isRunning = await browserLayer.waitForQuestionChange(currentContainerText);
98
+ questionNumber++;
99
+ }
100
+
101
+ console.log("\n[INFO] Quiz loop finished. Printing final report...");
102
+ analyzerLayer.printSummary();
103
+
104
+ } catch (error) {
105
+ console.error("Critical Error during execution:", error);
106
+ } finally {
107
+ await browserLayer.close();
108
+ }
109
+ }
110
+
111
+ main();
package/src/ocr.js ADDED
@@ -0,0 +1,78 @@
1
+ const Tesseract = require('tesseract.js');
2
+ const Jimp = require('jimp');
3
+ const path = require('path');
4
+
5
+ class OCRLayer {
6
+ constructor() {}
7
+
8
+ async preprocessImage(inputPath, outputPath) {
9
+ console.log(`[OCR] Preprocessing image: ${inputPath}`);
10
+ try {
11
+ // Read the image
12
+ const image = await Jimp.read(inputPath);
13
+
14
+ // Convert to grayscale, increase contrast/sharpness for better OCR
15
+ image.greyscale()
16
+ .contrast(0.5)
17
+ .normalize();
18
+
19
+ await image.writeAsync(outputPath);
20
+ return outputPath;
21
+ } catch (error) {
22
+ console.error("[OCR] Image preprocessing failed:", error);
23
+ // Return original if preprocessing fails
24
+ return inputPath;
25
+ }
26
+ }
27
+
28
+ async extractText(imagePath) {
29
+ console.log(`[OCR] Extracting text from: ${imagePath}`);
30
+ try {
31
+ const result = await Tesseract.recognize(
32
+ imagePath,
33
+ 'eng',
34
+ { logger: m => {} } // suppress verbose logging
35
+ );
36
+
37
+ const text = result.data.text;
38
+ return this.parseExtractedText(text);
39
+ } catch (error) {
40
+ console.error("[OCR] Tesseract extraction failed:", error);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ parseExtractedText(rawText) {
46
+ console.log("[OCR] Parsing extracted text...");
47
+ // Simple heuristic to split question from options
48
+ // Assuming options start with A), B), C), D) or A., B., C., D.
49
+ const lines = rawText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
50
+
51
+ let questionLines = [];
52
+ let options = [];
53
+
54
+ const optionRegex = /^[A-D][\)\.]\s*(.*)/i;
55
+
56
+ for (const line of lines) {
57
+ const match = line.match(optionRegex);
58
+ if (match) {
59
+ options.push(line);
60
+ } else {
61
+ if (options.length === 0) {
62
+ questionLines.push(line);
63
+ } else {
64
+ // It's a continuation of the last option, or trailing text.
65
+ // For simplicity, append to the last option if there is one.
66
+ options[options.length - 1] += ' ' + line;
67
+ }
68
+ }
69
+ }
70
+
71
+ return {
72
+ question: questionLines.join(' ').trim(),
73
+ options: options.map(o => o.trim())
74
+ };
75
+ }
76
+ }
77
+
78
+ module.exports = OCRLayer;
package/test/quiz.html ADDED
@@ -0,0 +1,156 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Sample Quiz</title>
5
+ <style>
6
+ body { font-family: sans-serif; padding: 20px; }
7
+ .question-container { margin-bottom: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 5px; position: relative; }
8
+ .question-text { font-size: 1.2em; font-weight: bold; margin-bottom: 10px; }
9
+ .option { margin-bottom: 5px; cursor: pointer; display: block; }
10
+ .stress-mode-canvas { display: none; }
11
+ #next-btn { padding: 10px 20px; font-size: 1em; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 5px; margin-top: 10px; }
12
+ #next-btn:hover { background-color: #0056b3; }
13
+ .completed { color: green; font-weight: bold; font-size: 1.2em; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <h1>Security Assessment Quiz</h1>
18
+
19
+ <div class="question-container" id="q-container">
20
+ <div class="question-text" id="qt-text">Loading...</div>
21
+ <div class="options-list" id="ol-list"></div>
22
+ </div>
23
+
24
+ <button id="next-btn">Next Question</button>
25
+
26
+ <script>
27
+ const questions = [
28
+ {
29
+ type: 'text',
30
+ q: "What is the primary purpose of Cross-Site Scripting (XSS)?",
31
+ opts: [
32
+ "A) To perform SQL injection",
33
+ "B) To execute arbitrary JavaScript in the victim's browser",
34
+ "C) To compromise the backend database",
35
+ "D) To sniff network traffic"
36
+ ]
37
+ },
38
+ {
39
+ type: 'text',
40
+ q: "Which of the following prevents Cross-Site Request Forgery (CSRF)?",
41
+ opts: [
42
+ "A) Anti-CSRF Tokens",
43
+ "B) Prepared Statements",
44
+ "C) Input Validation",
45
+ "D) HTTPS"
46
+ ]
47
+ },
48
+ {
49
+ type: 'canvas',
50
+ q: "Which HTTP header defends against Clickjacking?",
51
+ opts: [
52
+ "A) X-XSS-Protection",
53
+ "B) Content-Security-Policy",
54
+ "C) X-Frame-Options",
55
+ "D) Strict-Transport-Security"
56
+ ]
57
+ },
58
+ {
59
+ type: 'canvas',
60
+ q: "In what phase of a cyber attack does 'reconnaissance' occur?",
61
+ opts: [
62
+ "A) Exploitation",
63
+ "B) Delivery",
64
+ "C) Information Gathering",
65
+ "D) Maintaining Access"
66
+ ]
67
+ }
68
+ ];
69
+
70
+ let currentIndex = 0;
71
+
72
+ function loadQuestion(index) {
73
+ if (index >= questions.length) {
74
+ document.getElementById('q-container').innerHTML = "<div class='completed' id='qt-text'>Quiz Completed!</div>";
75
+ document.getElementById('next-btn').style.display = 'none';
76
+ return;
77
+ }
78
+
79
+ const q = questions[index];
80
+ const qContainer = document.getElementById('q-container');
81
+ const urlParams = new URLSearchParams(window.location.search);
82
+ const isStress = urlParams.get('stress') === 'true';
83
+
84
+ // If it's a natively OCR-based question (canvas), OR if we're in stress mode.
85
+ if (q.type === 'canvas' || isStress) {
86
+ console.log("Canvas rendering mode activated: forcing OCR fallback...");
87
+ qContainer.className = "container-xyz123"; // obfuscate container class
88
+
89
+ // We draw the question as an image so DOM text extractors fail completely.
90
+ // But we inject invisible radio buttons so the AI's "click simulation" still works.
91
+ let html = `<canvas id="q-canvas" width="600" height="300"></canvas>`;
92
+
93
+ let yPos = 80;
94
+ q.opts.forEach((optText) => {
95
+ const letter = optText.charAt(0);
96
+ // Invisible hit-boxes for the Playwright mouse to target
97
+ html += `<input type="radio" name="q" id="q_${letter}" style="opacity: 0.01; position: absolute; left: 30px; top: ${yPos - 10}px; width: 20px; height: 20px; z-index: 10;">`;
98
+ yPos += 40;
99
+ });
100
+
101
+ qContainer.innerHTML = html;
102
+
103
+ // Draw to canvas
104
+ const canvas = document.getElementById('q-canvas');
105
+ const ctx = canvas.getContext('2d');
106
+
107
+ // Draw background noise (makes basic scrapers suffer, but OCR handles it)
108
+ ctx.fillStyle = '#f0f0f0';
109
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
110
+ for(let i=0; i<500; i++) {
111
+ ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
112
+ ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 2, 2);
113
+ }
114
+
115
+ ctx.fillStyle = 'black';
116
+ ctx.font = 'bold 18px sans-serif';
117
+ ctx.fillText(q.q, 20, 40);
118
+
119
+ ctx.font = '16px sans-serif';
120
+ let currentY = 80;
121
+ q.opts.forEach(optText => {
122
+ ctx.beginPath();
123
+ ctx.arc(40, currentY - 5, 8, 0, 2 * Math.PI);
124
+ ctx.stroke();
125
+ ctx.fillText(optText, 60, currentY);
126
+ currentY += 40;
127
+ });
128
+
129
+ // We still need a hidden way for our `waitForQuestionChange` loop to know it changed.
130
+ // We'll add a hidden metadata div.
131
+ qContainer.innerHTML += `<div id="q-meta" style="display:none;">${q.q}</div>`;
132
+
133
+ } else {
134
+ // Standard DOM text mode
135
+ qContainer.className = "question-container";
136
+ let html = `<div class="question-text" id="qt-text">${q.q}</div>`;
137
+ html += `<div class="options-list" id="ol-list">`;
138
+ q.opts.forEach((optText) => {
139
+ const letter = optText.charAt(0);
140
+ html += `<div class="option" data-val="${letter}"><input type="radio" name="q" id="q_${letter}"><label for="q_${letter}">${optText}</label></div>`;
141
+ });
142
+ html += `</div>`;
143
+ qContainer.innerHTML = html;
144
+ }
145
+ }
146
+
147
+ document.getElementById('next-btn').addEventListener('click', () => {
148
+ currentIndex++;
149
+ loadQuestion(currentIndex);
150
+ });
151
+
152
+ // Initial load
153
+ loadQuestion(currentIndex);
154
+ </script>
155
+ </body>
156
+ </html>
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <div id="q-container"></div>
5
+ <script>
6
+ const qContainer = document.getElementById('q-container');
7
+ const q = {
8
+ q: "Which HTTP header defends against Clickjacking?",
9
+ opts: [
10
+ "A) X-XSS-Protection",
11
+ "B) Content-Security-Policy",
12
+ "C) X-Frame-Options",
13
+ "D) Strict-Transport-Security"
14
+ ]
15
+ };
16
+ let html = `<canvas id="q-canvas" width="600" height="300"></canvas>`;
17
+ let yPos = 80;
18
+ q.opts.forEach((optText) => {
19
+ const letter = optText.charAt(0);
20
+ html += `<input type="radio" name="q" id="q_${letter}" style="opacity: 0.01; position: absolute; left: 30px; top: ${yPos - 10}px; width: 20px; height: 20px; z-index: 10;">`;
21
+ yPos += 40;
22
+ });
23
+ qContainer.innerHTML = html;
24
+
25
+ const canvas = document.getElementById('q-canvas');
26
+ const ctx = canvas.getContext('2d');
27
+ ctx.fillStyle = '#f0f0f0';
28
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
29
+ for(let i=0; i<500; i++) {
30
+ ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
31
+ ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 2, 2);
32
+ }
33
+ ctx.fillStyle = 'black';
34
+ ctx.font = 'bold 18px sans-serif';
35
+ ctx.fillText(q.q, 20, 40);
36
+ ctx.font = '16px sans-serif';
37
+ let currentY = 80;
38
+ q.opts.forEach(optText => {
39
+ ctx.beginPath();
40
+ ctx.arc(40, currentY - 5, 8, 0, 2 * Math.PI);
41
+ ctx.stroke();
42
+ ctx.fillText(optText, 60, currentY);
43
+ currentY += 40;
44
+ });
45
+ </script>
46
+ </body>
47
+ </html>