diff --git a/src/node/NodeRecipe.mjs b/src/node/NodeRecipe.mjs index b623f611e..bad8fc274 100644 --- a/src/node/NodeRecipe.mjs +++ b/src/node/NodeRecipe.mjs @@ -24,20 +24,30 @@ class NodeRecipe { /** - * Validate an ingredient $ coerce to operation if necessary. + * Validate an ingredient & coerce to operation if necessary. * @param {String | Function | Object} ing + * @returns {Function || Object} The operation, or an object with the + * operation and its arguments + * @throws {TypeError} If it cannot find the operation in chef's list of operations. */ _validateIngredient(ing) { + // CASE operation name given. Find operation and validate if (typeof ing === "string") { const op = operations.find((op) => { return sanitise(op.opName) === sanitise(ing); }); if (op) { - return op; + // Need to validate against case 2 + return this._validateIngredient(op); } else { throw new TypeError(`Couldn't find an operation with name '${ing}'.`); } + // CASE operation given. Check its a chef operation and check its not flowcontrol } else if (typeof ing === "function") { + if (ing.flowControl) { + throw new TypeError(`flowControl operations like ${ing.opName} are not currently allowed in recipes for chef.bake in the Node API`); + } + if (operations.includes(ing)) { return ing; } else { @@ -57,7 +67,7 @@ class NodeRecipe { /** - * Parse config for recipe. + * Parse an opList from a recipeConfig and assign it to the recipe's opList. * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig */ _parseConfig(recipeConfig) { diff --git a/src/node/api.mjs b/src/node/api.mjs index 5733fb1d8..dcdb30f0a 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -177,6 +177,7 @@ export function _wrap(OpClass) { // Check to see if class's run function is async. const opInstance = new OpClass(); const isAsync = opInstance.run.constructor.name === "AsyncFunction"; + const isFlowControl = opInstance.flowControl; let wrapped; @@ -191,7 +192,28 @@ export function _wrap(OpClass) { */ wrapped = async (input, args=null) => { const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + + // SPECIAL CASE for Magic. Other flowControl operations will + // not work because the opList is not passed in. + if (isFlowControl) { + opInstance.ingValues = transformedArgs; + + const state = { + progress: 0, + dish: ensureIsDish(transformedInput), + opList: [opInstance], + }; + + const updatedState = await opInstance.run(state); + + return new NodeDish({ + value: updatedState.dish.value, + type: opInstance.outputType, + }); + } + const result = await opInstance.run(transformedInput, transformedArgs); + return new NodeDish({ value: result, type: opInstance.outputType, @@ -218,6 +240,8 @@ export function _wrap(OpClass) { // used in chef.help wrapped.opName = OpClass.name; wrapped.args = createArgInfo(opInstance); + // Used in NodeRecipe to check for flowControl ops + wrapped.flowControl = isFlowControl; return wrapped; } @@ -292,25 +316,18 @@ export function help(input) { /** - * bake [Wrapped] - Perform an array of operations on some input. - * @returns {Function} + * bake + * + * @param {*} input - some input for a recipe. + * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig - + * An operation, operation name, or an array of either. + * @returns {NodeDish} of the result + * @throws {TypeError} if invalid recipe given. */ -export function bake() { - - /** - * bake - * - * @param {*} input - some input for a recipe. - * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig - - * An operation, operation name, or an array of either. - * @returns {SyncDish} of the result - * @throws {TypeError} if invalid recipe given. - */ - return function(input, recipeConfig) { - const recipe = new NodeRecipe(recipeConfig); - const dish = ensureIsDish(input); - return recipe.execute(dish); - }; +export function bake(input, recipeConfig) { + const recipe = new NodeRecipe(recipeConfig); + const dish = ensureIsDish(input); + return recipe.execute(dish); } diff --git a/src/node/config/scripts/generateNodeIndex.mjs b/src/node/config/scripts/generateNodeIndex.mjs index 81ca8fe63..13c9a842d 100644 --- a/src/node/config/scripts/generateNodeIndex.mjs +++ b/src/node/config/scripts/generateNodeIndex.mjs @@ -100,8 +100,7 @@ Object.keys(operations).forEach((op) => { code += `]; -const prebaked = bake(operations); -chef.bake = prebaked; +chef.bake = bake; export default chef; // Operations as top level exports. @@ -114,7 +113,7 @@ Object.keys(operations).forEach((op) => { }); code += " NodeDish as Dish,\n"; -code += " prebaked as bake,\n"; +code += " bake,\n"; code += " help,\n"; code += " OperationError,\n"; code += " ExcludedOperationError,\n"; diff --git a/tests/lib/TestRegister.mjs b/tests/lib/TestRegister.mjs index 0236fc9f0..452f3f551 100644 --- a/tests/lib/TestRegister.mjs +++ b/tests/lib/TestRegister.mjs @@ -152,7 +152,7 @@ class TestRegister { result.status = "passing"; } catch (e) { result.status = "erroring"; - result.output = e.message; + result.output = `${e.message}\nError: ${e.stack}`; } testResults.push(result); diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index a4e907b89..52f79330c 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -13,10 +13,10 @@ import assert from "assert"; import it from "../assertionHandler.mjs"; import chef from "../../../src/node/index.mjs"; -import OperationError from "../../../src/core/errors/OperationError.mjs"; +import { OperationError, ExcludedOperationError } from "../../../src/core/errors/index.mjs"; import NodeDish from "../../../src/node/NodeDish.mjs"; -import { toBase32} from "../../../src/node/index.mjs"; +import { toBase32, magic} from "../../../src/node/index.mjs"; import TestRegister from "../../lib/TestRegister.mjs"; TestRegister.addApiTests([ @@ -181,22 +181,17 @@ TestRegister.addApiTests([ }), it("chef.bake: should complain if recipe isnt a valid object", () => { - try { - chef.bake("some input", 3264); - } catch (e) { - assert.strictEqual(e.name, "TypeError"); - assert.strictEqual(e.message, "Recipe can only contain function names or functions"); - } + assert.throws(() => chef.bake("some input", 3264), { + name: "TypeError", + message: "Recipe can only contain function names or functions" + }); }), it("chef.bake: Should complain if string op is invalid", () => { - try { - chef.bake("some input", "not a valid operation"); - assert.fail("Shouldn't be hit"); - } catch (e) { - assert.strictEqual(e.name, "TypeError"); - assert.strictEqual(e.message, "Couldn't find an operation with name 'not a valid operation'."); - } + assert.throws(() => chef.bake("some input", "not a valid operation"), { + name: "TypeError", + message: "Couldn't find an operation with name 'not a valid operation'." + }); }), it("chef.bake: Should take an input and an operation and perform it", () => { @@ -205,13 +200,10 @@ TestRegister.addApiTests([ }), it("chef.bake: Should complain if an invalid operation is inputted", () => { - try { - chef.bake("https://google.com/search?q=help", () => {}); - assert.fail("Shouldn't be hit"); - } catch (e) { - assert.strictEqual(e.name, "TypeError"); - assert.strictEqual(e.message, "Inputted function not a Chef operation."); - } + assert.throws(() => chef.bake("https://google.com/search?q=help", () => {}), { + name: "TypeError", + message: "Inputted function not a Chef operation." + }); }), it("chef.bake: accepts an array of operation names and performs them all in order", () => { @@ -241,12 +233,10 @@ TestRegister.addApiTests([ }), it("should complain if an invalid operation is inputted as part of array", () => { - try { - chef.bake("something", [() => {}]); - } catch (e) { - assert.strictEqual(e.name, "TypeError"); - assert.strictEqual(e.message, "Inputted function not a Chef operation."); - } + assert.throws(() => chef.bake("something", [() => {}]), { + name: "TypeError", + message: "Inputted function not a Chef operation." + }); }), it("chef.bake: should take single JSON object describing op and args OBJ", () => { @@ -275,15 +265,13 @@ TestRegister.addApiTests([ }), it("chef.bake: should error if op in JSON is not chef op", () => { - try { - chef.bake("some input", { - op: () => {}, - args: ["Colon"], - }); - } catch (e) { - assert.strictEqual(e.name, "TypeError"); - assert.strictEqual(e.message, "Inputted function not a Chef operation."); - } + assert.throws(() => chef.bake("some input", { + op: () => {}, + args: ["Colon"], + }), { + name: "TypeError", + message: "Inputted function not a Chef operation." + }); }), it("chef.bake: should take multiple ops in JSON object form, some ops by string", () => { @@ -357,22 +345,38 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something"); }), - it("Excluded operations: throw a sensible error when you try and call one", () => { - try { - chef.fork(); - } catch (e) { - assert.strictEqual(e.type, "ExcludedOperationError"); - assert.strictEqual(e.message, "Sorry, the Fork operation is not available in the Node.js version of CyberChef."); - } + it("chef.bake: cannot accept flowControl operations in recipe", () => { + assert.throws(() => chef.bake("some input", "magic"), { + name: "TypeError", + message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" + }); + assert.throws(() => chef.bake("some input", magic), { + name: "TypeError", + message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" + }); + assert.throws(() => chef.bake("some input", ["to base 64", "magic"]), { + name: "TypeError", + message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" + }); }), it("Excluded operations: throw a sensible error when you try and call one", () => { - try { - chef.renderImage(); - } catch (e) { - assert.strictEqual(e.type, "ExcludedOperationError"); - assert.strictEqual(e.message, "Sorry, the RenderImage operation is not available in the Node.js version of CyberChef."); - } + assert.throws(chef.fork, + (err) => { + assert(err instanceof ExcludedOperationError); + assert.deepEqual(err.message, "Sorry, the Fork operation is not available in the Node.js version of CyberChef."); + return true; + }, + "Unexpected error type" + ); + assert.throws(chef.javaScriptBeautify, + (err) => { + assert(err instanceof ExcludedOperationError); + assert.deepEqual(err.message, "Sorry, the JavaScriptBeautify operation is not available in the Node.js version of CyberChef."); + return true; + }, + "Unexpected error type" + ); }), it("Operation arguments: should be accessible from operation object if op has array arg", () => { @@ -405,4 +409,5 @@ TestRegister.addApiTests([ assert.equal(chef.convertDistance.args.inputUnits.options[0], "Nanometres (nm)"); assert.equal(chef.defangURL.args.process.options[1], "Only full URLs"); }), + ]); diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs index baf3f238a..2124ad141 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -1075,6 +1075,60 @@ ExifImageHeight: 57`); assert.equal(output, res.value); }), + it("performs MAGIC", async () => { + const input = "WUagwsiae6mP8gNtCCLUFpCpCB26RmBDoDD8PacdAmzAzBVjkK2QstFXaKhpC6iUS7RHqXrJtFisoRSgoJ4whjm1arm864qaNq4RcfUmLHrcsAaZc5TXCYifNdgS83gDeejGX46gaiMyuBV6EskHt1scgJ88x2tNSotQDwbGY1mmCob2ARGFvCKYNqiN9ipMq1ZU1mgkdbNuGcb76aRtYWhCGUc8g93UJudhb8htsheZnwTpgqhx83SVJSZXMXUjJT2zmpC7uXWtumqokbdSi88YtkWDAc1Toouh2oH4D4ddmNKJWUDpMwmngUmK14xwmomccPQE9hM172APnSqwxdKQ172RkcAsysnmj5gGtRmVNNh2s359wr6mS2QRP"; + const depth = 1; + + const res = await chef.magic(input, { + depth, + }); + + // assert against the structure of the output, rather than the values. + assert.strictEqual(res.value.length, depth + 1); + res.value.forEach(row => { + assert.ok(row.recipe); + assert.ok(row.data); + assert.ok(row.languageScores); + assert.ok(Object.prototype.hasOwnProperty.call(row, "fileType")); // Can be null, so cannot just use ok + assert.ok(row.entropy); + assert.ok(row.matchingOps); + assert.ok(Object.prototype.hasOwnProperty.call(row, "useful")); + assert.ok(Object.prototype.hasOwnProperty.call(row, "matchesCrib")); + + row.recipe.forEach(item => { + assert.ok(Object.prototype.hasOwnProperty.call(item, "op"), `No 'op' property in item ${item}`); + assert.strictEqual(typeof item.op, "string"); + assert.ok(Object.prototype.hasOwnProperty.call(item, "args"), `No 'args' property in item ${item}`); + assert.ok(Array.isArray(item.args)); + }); + + row.languageScores.forEach(score => { + assert.ok(Object.prototype.hasOwnProperty.call(score, "lang"), `No 'lang' property in languageScore ${score}`); + assert.strictEqual(typeof score.lang, "string"); + assert.ok(Object.prototype.hasOwnProperty.call(score, "score"), `No 'score' property in languageScore ${score}`); + assert.strictEqual(typeof score.score, "number"); + assert.ok(Object.prototype.hasOwnProperty.call(score, "probability"), `No 'probability' property in languageScore ${score}`); + assert.strictEqual(typeof score.probability, "number"); + }); + + row.matchingOps.forEach(op => { + assert.ok(Object.prototype.hasOwnProperty.call(op, "op"), `No 'op' property in matchingOp ${JSON.stringify(op)}`); + assert.strictEqual(typeof op.op, "string"); + assert.ok(Object.prototype.hasOwnProperty.call(op, "pattern"), `No 'pattern' property in matchingOp ${JSON.stringify(op)}`); + assert.ok(op.pattern instanceof RegExp); + assert.ok(Object.prototype.hasOwnProperty.call(op, "args"), `No 'args' property in matchingOp ${JSON.stringify(op)}`); + assert.ok(Array.isArray(op.args)); + assert.ok(Object.prototype.hasOwnProperty.call(op, "useful"), `No 'useful' property in matchingOp ${JSON.stringify(op)}`); + assert.ifError(op.useful); // Expect this to be undefined + assert.ok(Object.prototype.hasOwnProperty.call(op, "entropyRange"), `No 'entropyRange' property in matchingOp ${JSON.stringify(op)}`); + assert.ifError(op.entropyRange); // Expect this to be undefined + assert.ok(Object.prototype.hasOwnProperty.call(op, "output"), `No 'output' property in matchingOp ${JSON.stringify(op)}`); + assert.ifError(op.output); // Expect this to be undefined + }); + }); + + }), + ]);