Skip to content

Commit

Permalink
feat: convert Javascript code to Typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
nielm committed Dec 5, 2024
1 parent 46fd714 commit e0dd556
Show file tree
Hide file tree
Showing 19 changed files with 908 additions and 675 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/codehealth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ jobs:
working-directory: cloudrun-malware-scanner/
run: npm run check-format

- name: Typescript checks
working-directory: cloudrun-malware-scanner/
run: npm run typecheck

- name: NPM test
- name: NPM compile and test
working-directory: cloudrun-malware-scanner/
run: npm test

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ config.json
.terraform
**/.terraform/*
.terraform.tfstate.lock.info
build
1 change: 1 addition & 0 deletions cloudrun-malware-scanner/.gcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.gcloudignore
.eslintrc.js
config.json.tmpl
build
3 changes: 1 addition & 2 deletions cloudrun-malware-scanner/.husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ echo "Running cloudrun-malware-scanner/.husky/pre-commit checks. Use -n/--no-ver
cd cloudrun-malware-scanner || exit 1
npm run eslint
npm run check-format
npm run typecheck
npm run terraform-validate
npm test
npm audit
npm run terraform-validate
1 change: 1 addition & 0 deletions cloudrun-malware-scanner/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ CHANGELOG.md
../.release-please-manifest.json
../terraform/*/.terraform
../terraform/*/terraform.tfstate*
build
4 changes: 2 additions & 2 deletions cloudrun-malware-scanner/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ RUN set -x \
WORKDIR /app
COPY . /app

# Install required NPM modules
RUN npm install --omit=dev
# Install required NPM modules and build
RUN npm install && npm run build

CMD ["bash", "bootstrap.sh"]
45 changes: 45 additions & 0 deletions cloudrun-malware-scanner/clamdjs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Typescript module typings for clamdjs npm
*/
declare module 'clamdjs' {
export type ScanStreamFunc = (
stream: import('node:stream').Readable,
timeout: number,
) => Promise<string>;

export interface IScanner {
scanStream: ScanStreamFunc;
}

export function createScanner(host: string, port: number): IScanner;

export function ping(
host: string,
port: number,
timeout?: number,
): Promise<boolean>;

export function version(
host: string,
port: number,
timeout?: number,
): Promise<string>;

export function isCleanReply(reply: any): boolean;
}
150 changes: 72 additions & 78 deletions cloudrun-malware-scanner/config.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,47 @@
/*
* Copyright 2022 Google LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const {logger} = require('./logger.js');
const {readFileSync} = require('node:fs');

/** @typedef {import('@google-cloud/storage').Storage} Storage */

/**
* @typedef {{
* unscanned: string,
* clean: string,
* quarantined: string,
* }} BucketDefs
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** @type {Array<keyof BucketDefs>} */
const BUCKET_TYPES = ['unscanned', 'clean', 'quarantined'];
import {logger} from './logger';
import {readFileSync} from 'node:fs';
import {Storage} from '@google-cloud/storage';

/**
* Configuration object.
*
* Values are read from the JSON configuration file.
* See {@link readAndVerifyConfig}.
*
* @typedef {{
* buckets: Array<BucketDefs>,
* ClamCvdMirrorBucket: string,
* fileExclusionPatterns?: Array<string | Array<string>>,
* fileExclusionRegexps: Array<RegExp>,
* ignoreZeroLengthFiles: boolean,
* comments?: string | string[]
* }} Config
*/
export type BucketDefs = {
unscanned: string;
clean: string;
quarantined: string;
};

export type Config = {
buckets: Array<BucketDefs>;
ClamCvdMirrorBucket: string;
fileExclusionPatterns?: Array<string | Array<string>>;
fileExclusionRegexps: Array<RegExp>;
ignoreZeroLengthFiles: boolean;
comments?: string | string[];
};

const BUCKET_TYPES = ['unscanned', 'clean', 'quarantined'] as Array<
keyof BucketDefs
>;

/**
* Read configuration from JSON configuration file, parse, verify
Expand All @@ -55,42 +52,40 @@ const BUCKET_TYPES = ['unscanned', 'clean', 'quarantined'];
* @param {Storage} storage
* @return {Promise<Config>}
*/
async function readAndVerifyConfig(configFile, storage) {
export async function readAndVerifyConfig(
configFile: string,
storage: Storage,
): Promise<Config> {
logger.info(`Using configuration file: ${configFile}`);
let configText;
try {
configText = readFileSync(configFile, {encoding: 'utf-8'});
} catch (e) {
const err = /** @type {Error} */ (e);
} catch (e: any) {
logger.fatal(
err,
`Unable to read JSON file from ${configFile}: ${err.message}`,
e,
`Unable to read JSON file from ${configFile}: ${e.message}`,
);
throw err;
throw e;
}
try {
return validateConfig(parseConfig(configText), storage);
} catch (e) {
const err = /** @type {Error} */ (e);
logger.fatal(
err,
`Failed parsing config file: ${configFile}: ${err.message}`,
);
throw err;
} catch (e: any) {
logger.fatal(e, `Failed parsing config file: ${configFile}: ${e.message}`);
throw e;
}
}

/**
* @param {string} configText
* @returns {Config}
*/
function parseConfig(configText) {
function parseConfig(configText: string): Config {
/** @type {Config} */
let config;

try {
config = JSON.parse(configText);
} catch (e) {
} catch (e: any) {
throw new Error(`Failed to parse configuration as JSON: ${e}`);
}
return config;
Expand All @@ -105,7 +100,7 @@ function parseConfig(configText) {
* @param {Storage} storage
* @return {Promise<Config>}
*/
async function validateConfig(config, storage) {
async function validateConfig(config: any, storage: Storage): Promise<Config> {
delete config.comments;

if (config.buckets.length === 0) {
Expand Down Expand Up @@ -171,28 +166,26 @@ async function validateConfig(config, storage) {
// config.fileExclusionPatterns is an array, check each value and
// convert to a regexp in fileExclusionRegexps[]
for (const i in config.fileExclusionPatterns) {
/** @type {string|undefined} */
let pattern;
/** @type {string|undefined} */
let flags;
let pattern: string | undefined;
let flags: string | undefined;

// Each element can either be a simple pattern:
// "^.*\\.tmp$"
// or an array with pattern and flags, eg for case-insensive matching:
// [ "^.*\\tmp$", "i" ]

if (typeof config.fileExclusionPatterns[i] === 'string') {
const element = config.fileExclusionPatterns[i];
if (typeof element === 'string') {
// validate regex as simple string
pattern = config.fileExclusionPatterns[i];
pattern = element;
} else if (
config.fileExclusionPatterns[i] instanceof Array &&
config.fileExclusionPatterns[i].length <= 2 &&
config.fileExclusionPatterns[i].length >= 1 &&
typeof config.fileExclusionPatterns[i][0] === 'string'
Array.isArray(element) &&
element.length <= 2 &&
element.length >= 1 &&
typeof element[0] === 'string'
) {
// validate regex as [pattern, flags]
pattern = config.fileExclusionPatterns[i][0];
flags = config.fileExclusionPatterns[i][1];
pattern = element[0];
flags = element[1];
} else {
pattern = undefined;
}
Expand All @@ -205,11 +198,10 @@ async function validateConfig(config, storage) {
} else {
try {
config.fileExclusionRegexps[i] = new RegExp(pattern, flags);
} catch (e) {
const err = /** @type {Error} */ (e);
} catch (e: any) {
logger.fatal(
err,
`Config Error: fileExclusionPatterns[${i}]: Regexp compile failed for ${JSON.stringify(config.fileExclusionPatterns[i])}: ${err.message}`,
e,
`Config Error: fileExclusionPatterns[${i}]: Regexp compile failed for ${JSON.stringify(config.fileExclusionPatterns[i])}: ${e.message}`,
);
success = false;
}
Expand All @@ -222,6 +214,7 @@ async function validateConfig(config, storage) {
if (!success) {
throw new Error('Invalid configuration');
}

return Object.freeze(config);
}

Expand All @@ -233,7 +226,11 @@ async function validateConfig(config, storage) {
* @param {Storage} storage
* @return {Promise<boolean>}
*/
async function checkBucketExists(bucketName, configName, storage) {
async function checkBucketExists(
bucketName: string,
configName: string,
storage: Storage,
): Promise<boolean> {
if (!bucketName) {
logger.fatal(`Config Error: no "${configName}" bucket defined`);
return false;
Expand All @@ -247,7 +244,7 @@ async function checkBucketExists(bucketName, configName, storage) {
.bucket(bucketName)
.getFiles({maxResults: 1, prefix: 'zzz', autoPaginate: false});
return true;
} catch (e) {
} catch (e: any) {
logger.fatal(
`Error in config: cannot view files in "${configName}" : ${bucketName} : ${e}`,
);
Expand All @@ -256,10 +253,7 @@ async function checkBucketExists(bucketName, configName, storage) {
}
}

module.exports = {
readAndVerifyConfig,
TEST_ONLY: {
checkBucketExists,
validateConfig,
},
export const TEST_ONLY = {
checkBucketExists,
validateConfig,
};
Loading

0 comments on commit e0dd556

Please sign in to comment.