Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow magic in node api #1049

Merged
merged 3 commits into from
Jun 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/node/NodeRecipe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
53 changes: 35 additions & 18 deletions src/node/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}


Expand Down
5 changes: 2 additions & 3 deletions src/node/config/scripts/generateNodeIndex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/TestRegister.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
105 changes: 55 additions & 50 deletions tests/node/tests/nodeApi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
}),

]);
54 changes: 54 additions & 0 deletions tests/node/tests/operations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});

}),


]);