diff --git a/packages/scanner/doc/labels/crypto.decrypt.md b/packages/scanner/doc/labels/crypto.decrypt.md new file mode 100644 index 0000000000..456bd1af18 --- /dev/null +++ b/packages/scanner/doc/labels/crypto.decrypt.md @@ -0,0 +1,7 @@ +--- +name: crypto.decrypt +rules: + - hard-coded-key +--- + +A function that performs decryption. diff --git a/packages/scanner/doc/labels/crypto.encrypt.md b/packages/scanner/doc/labels/crypto.encrypt.md index 547151020d..3ba15eeb62 100644 --- a/packages/scanner/doc/labels/crypto.encrypt.md +++ b/packages/scanner/doc/labels/crypto.encrypt.md @@ -1,6 +1,7 @@ --- name: crypto.encrypt rules: + - hard-coded-key - unauthenticated-encryption --- diff --git a/packages/scanner/doc/labels/crypto.set_key.md b/packages/scanner/doc/labels/crypto.set_key.md new file mode 100644 index 0000000000..7b4b4cb6ed --- /dev/null +++ b/packages/scanner/doc/labels/crypto.set_key.md @@ -0,0 +1,7 @@ +--- +name: crypto.set_key +rules: + - hard-coded-key +--- + +A function that sets the crytographic key for encryption, decryption, or signature. diff --git a/packages/scanner/doc/labels/string.unpack.md b/packages/scanner/doc/labels/string.unpack.md new file mode 100644 index 0000000000..9a623158f3 --- /dev/null +++ b/packages/scanner/doc/labels/string.unpack.md @@ -0,0 +1,12 @@ +--- +name: string.unpack +rules: + - hard-coded-key +--- + +Unpacks a string into binary from a printable encoding such as hex or Base64. + +## Examples + +- Ruby [String#unpack](https://ruby-doc.org/core-3.1.2/String.html#method-i-unpack) +- Ruby [String#unpack1](https://ruby-doc.org/core-3.1.2/String.html#method-i-unpack1) diff --git a/packages/scanner/doc/rules/hard-coded-key.md b/packages/scanner/doc/rules/hard-coded-key.md new file mode 100644 index 0000000000..7b5b24ff1a --- /dev/null +++ b/packages/scanner/doc/rules/hard-coded-key.md @@ -0,0 +1,14 @@ +--- +rule: hard-coded-key +name: Hard coded key +title: Hard-coded key +references: + A02:2021: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/ +impactDomain: Security +labels: + - crypto.encrypt + - crypto.decrypt + - crypto.set_key + - string.unpack +scope: root +--- \ No newline at end of file diff --git a/packages/scanner/src/rules/hard-coded-key/metadata.ts b/packages/scanner/src/rules/hard-coded-key/metadata.ts new file mode 100644 index 0000000000..9835346136 --- /dev/null +++ b/packages/scanner/src/rules/hard-coded-key/metadata.ts @@ -0,0 +1,12 @@ +import { Metadata } from '../lib/metadata'; + +export default { + title: 'Hard-coded key', + scope: 'root', + enumerateScope: true, + impactDomain: 'Security', + references: { + 'A02:2021': 'https://owasp.org/Top10/A02_2021-Cryptographic_Failures/', + }, + labels: ['crypto.encrypt', 'crypto.decrypt', 'crypto.set_key', 'string.unpack'], +} as Metadata; diff --git a/packages/scanner/src/rules/hard-coded-key/rule.ts b/packages/scanner/src/rules/hard-coded-key/rule.ts new file mode 100644 index 0000000000..6e2ec1bdcc --- /dev/null +++ b/packages/scanner/src/rules/hard-coded-key/rule.ts @@ -0,0 +1,44 @@ +import { Event } from '@appland/models'; +import { AppMapIndex, MatcherResult, RuleLogic } from '../../types'; + +function matcher(event: Event, appMapIndex: AppMapIndex): MatcherResult { + if (!event.receiver) return; + + const receiverObjectId = event.receiver.object_id; + const setKey = appMapIndex.appMap.events.find( + (evt) => + evt.isCall() && + evt.receiver?.object_id === receiverObjectId && + evt.labels.has('crypto.set_key') + ); + if (!setKey) return; + + const keyObject = setKey.parameters![0]; + if (!keyObject) return; + + const keyObjectId = keyObject.object_id; + const obtainKey = appMapIndex.appMap.events.find( + (evt) => + evt.isReturn() && + evt !== setKey.returnEvent && + !evt.codeObject.labels.has('string.unpack') && + evt.returnValue?.object_id === keyObjectId + ); + if (!obtainKey) { + return [ + { + level: 'warning', + event, + message: `Cryptographic key is not obtained from a function, and may be hard-coded`, + participatingEvents: { 'crypto.set_key': setKey }, + }, + ]; + } +} + +export default function rule(): RuleLogic { + return { + matcher, + where: (e: Event) => e.labels.has('crypto.encrypt'), + }; +} diff --git a/packages/scanner/src/sampleConfig/default.yml b/packages/scanner/src/sampleConfig/default.yml index 97cf07ee15..01badedb72 100644 --- a/packages/scanner/src/sampleConfig/default.yml +++ b/packages/scanner/src/sampleConfig/default.yml @@ -3,6 +3,7 @@ checks: # - rule: circular-dependency - rule: deserialization-of-untrusted-data - rule: exec-of-untrusted-command + - rule: hard-coded-key - rule: http-500 # - rule: illegal-package-dependency # - rule: incompatible-http-client-request diff --git a/packages/scanner/test/scanner/hardCodedKey.spec.ts b/packages/scanner/test/scanner/hardCodedKey.spec.ts new file mode 100644 index 0000000000..60e0cf13fa --- /dev/null +++ b/packages/scanner/test/scanner/hardCodedKey.spec.ts @@ -0,0 +1,29 @@ +import Check from '../../src/check'; +import { loadRule } from '../../src/configuration/configurationProvider'; +import { scan } from '../util'; + +describe('hard-coded key', () => { + it('found', async () => { + const rule = await loadRule('hard-coded-key'); + const { findings } = await scan( + new Check(rule), + './ruby/fixture/tmp/appmap/minitest/Crypt_hard_coded_key.appmap.json' + ); + expect(findings).toHaveLength(1); + const finding1 = findings[0]; + expect(finding1.ruleId).toEqual('hard-coded-key'); + expect(finding1.event.id).toEqual(4); + expect(finding1.message).toEqual( + `Cryptographic key is not obtained from a function, and may be hard-coded` + ); + expect(finding1.relatedEvents).toHaveLength(2); + }); + it('not found', async () => { + const rule = await loadRule('hard-coded-key'); + const { findings } = await scan( + new Check(rule), + './ruby/fixture/tmp/appmap/minitest/Crypt_random_key.appmap.json' + ); + expect(findings).toHaveLength(0); + }); +});