secureCodeBox/secureCodeBox

View on GitHub
documentation/src/docs.build.js

Summary

Maintainability
C
1 day
Test Coverage
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

const fs = require("fs"),
  rimraf = require("rimraf"),
  colors = require("colors"),
  matter = require("gray-matter"),
  path = require('path'),
  { docsConfig: config } = require("./utils/config"),
  { removeWhitespaces } = require("./utils/capitalizer"),
  Mustache = require("mustache");


colors.setTheme({
  info: "blue",
  help: "cyan",
  warn: "yellow",
  success: "green",
  error: "red",
});

// For the documentation on this script look at the README.md of this repository

async function main() {
  const currentDirectory = __dirname; // current directory is /documentation/src
  const parentDirectory = path.dirname(currentDirectory); // parent is /documentation
  const rootDirectory = path.dirname(parentDirectory); // root is /

  const dataArray = await Promise.all(
    config.filesFromRepository.map((dir) =>
      readDirectory(`${rootDirectory}/${dir.src}`, false)
        .then((res) => ({ ...dir, files: res }))
        .catch((err) =>
          console.error(
            `ERROR: Could not read directory at: ${dir}`.error,
            err.message.error
          )
        )
    )
  );

  if (!fs.existsSync(config.targetPath)) {
    fs.mkdirSync(config.targetPath);
  }
  // Clear preexisting findings
  if (fs.existsSync(config.findingsDir)) {
    rimraf.sync(config.findingsDir);
  }

  for (const dir of dataArray) {
    const trgDir = `${config.targetPath}/${dir.dst}`;
    const srcDir = `${rootDirectory}/${dir.src}`;

    // Clears existing md files from directories
    if (fs.existsSync(trgDir)) {
      await removeExistingMarkdownFilesFromDirectory(trgDir, dir.keep);

      console.warn(
        `WARN: ${trgDir.info} already existed and was overwritten.`.warn
      );
    } else {
      fs.mkdirSync(trgDir);
    }

    // If the source directory contains a ".helm-docs.gotmpl" file (such as in /scanners or /hooks), the doc files need to be generated.
    // Else, the docs files are just copied to the destination path.
    dir.files.includes(".helm-docs.gotmpl")
      ? await createDocFilesFromMainRepository(srcDir, trgDir, await readDirectory(srcDir))
      : await copyFilesFromMainRepository(srcDir, trgDir, dir.exclude, dir.keep);
  }
}

main().catch((err) => {
  clearDocsOnFailure();
  console.error(err.stack.error);
});

function readDirectory(dir, dirNamesOnly = true) {
  return new Promise((res, rej) => {
    fs.readdir(
      dir,
      { encoding: "utf8", withFileTypes: true },
      function (err, data) {
        if (err) {
          rej(err);
        } else {
          if (dirNamesOnly) data = data.filter((file) => file.isDirectory());
          const directories = data.map((dirent) => dirent.name);
          res(directories);
        }
      }
    );
  });
}

async function createDocFilesFromMainRepository(relPath, targetPath, dirNames) {
  for (const dirName of dirNames) {
    const readMe = `${relPath}/${dirName}/README.md`;

    if (!fs.existsSync(readMe)) {
      console.log(
        `WARN: Skipping ${dirName.help}: file not found at ${readMe.info}.`.warn
      );
      continue;
    }

    // Read readme content of scanner / hook directory
    const readmeContent = fs.readFileSync(readMe, { encoding: "utf8" });

    const examples = await getExamples(`${relPath}/${dirName}/examples`);

    const imageTypes = await getSupportedImageTypes(`${relPath}/${dirName}/Chart.yaml`);

    // Add a custom editUrl to the frontMatter to ensure that it points to the correct repo
    const { data: frontmatter, content } = matter(readmeContent);
    
    // Either the path contains "secureCodeBox" or "repo" depending on whether the docs are locally generated or in netlify 
    const filePathInRepo = relPath.replace(/^.*(?:secureCodeBox|repo)\//, "");
    const readmeWithEditUrl = matter.stringify(content, {
      ...frontmatter,
      description: frontmatter?.usecase,
      custom_edit_url: `https://github.com/${config.repository}/edit/${config.branch}/${filePathInRepo}/${dirName}/.helm-docs.gotmpl`,
    });

    // Skip File if its marked as "hidden" in its frontmatter
    if (frontmatter.hidden !== undefined && frontmatter.hidden === true) {
      continue;
    }

    const integrationPage = Mustache.render(
      fs.readFileSync(path.join(__dirname, "utils/scannerReadme.mustache"), {
        encoding: "utf8",
      }),
      {
        readme: readmeWithEditUrl,
        examples,
        hasExamples: examples.length !== 0,
        imageTypes,
        hasImageTypes: imageTypes?.length > 0
      }
    );

    let fileName = frontmatter.title ? frontmatter.title : dirName;

    //Replace Spaces in the FileName with "-" and convert to lower case to avoid URL issues
    fileName = fileName.replace(/ /g, "-").toLowerCase();

    const filePath = `${targetPath}/${fileName}.md`;
    fs.writeFileSync(filePath, integrationPage);

    console.log(
      `SUCCESS: Created file for ${dirName.help} at ${filePath.info}`.success
    );
  }
}

async function getExamples(dir) {
  if (!fs.existsSync(dir)) {
    return [];
  }

  const dirNames = await readDirectory(dir).catch(() => []);

  if (dirNames.length === 0) {
    console.warn(`WARN: Found empty examples folder at ${dir.info}.`.warn);
    return [];
  }

  return dirNames.map((dirName) => {
    let readMe = "";

    if (fs.existsSync(`${dir}/${dirName}/README.md`)) {
      readMe = matter(
        fs.readFileSync(`${dir}/${dirName}/README.md`, {
          encoding: "utf8",
        })
      ).content;
    }

    let scanContent = null;
    if (fs.existsSync(`${dir}/${dirName}/scan.yaml`)) {
      scanContent = fs.readFileSync(`${dir}/${dirName}/scan.yaml`, {
        encoding: "utf8",
      });
    }

    let findingContent = null;
    let findingSizeLimitReached = null;

    if (fs.existsSync(`${dir}/${dirName}/findings.yaml`)) {
      findingSizeLimitReached =
        fs.statSync(`${dir}/${dirName}/findings.yaml`).size >= config.sizeLimit;

      if (findingSizeLimitReached) {
        console.warn(
          `WARN: Findings for ${dirName.info} exceeded size limit.`.warn
        );

        findingContent = copyFindingsForDownload(
          `${dir}/${dirName}/findings.yaml`
        );
      } else {
        findingContent = fs.readFileSync(`${dir}/${dirName}/findings.yaml`, {
          encoding: "utf8",
        });
      }
    }

    let findings = null;
    if (findingContent && findingSizeLimitReached !== null) {
      findings = {
        value: findingContent,
        limitReached: findingSizeLimitReached,
      };
    }

    return {
      name: removeWhitespaces(dirName),
      exampleReadme: readMe,
      scan: scanContent,
      findings,
    };
  });
}

function getSupportedImageTypes(dir) {
  if (fs.existsSync(dir)) {
    const chartContent = fs.readFileSync(dir, {
      encoding: "utf8",
    });

    // add an opening delimiter to help matter distinguish the file type
    const { data: frontmatter} = matter(['---', ...chartContent.toString().split('\n')].join('\n'));

    if ('annotations' in frontmatter && 'supported-platforms' in frontmatter.annotations) {
     return frontmatter['annotations']['supported-platforms'].split(',');
    }
  }
}

function copyFindingsForDownload(filePath) {
  const dirNames = filePath.split("/"),
    name =
      dirNames[dirNames.indexOf("examples") - 1] +
      "-" +
      dirNames[dirNames.indexOf("examples") + 1],
    targetPath = `/${config.findingsDir}/${name}-findings.yaml`;

  if (!fs.existsSync("static")) {
    fs.mkdirSync("static/");
  }
  if (!fs.existsSync(`static/${config.findingsDir}`)) {
    fs.mkdirSync(`static/${config.findingsDir}`);
  }

  fs.copyFileSync(filePath, "static" + targetPath);
  console.log(`SUCCESS: Created download link for ${name.info}.`.success);

  return targetPath;
}

function clearDocsOnFailure() {
  for (const dir of config.filesFromRepository) {
    const trgDir = `${config.targetPath}/${dir.src}`;

    if (fs.existsSync(trgDir)) {
      removeExistingMarkdownFilesFromDirectory(trgDir, dir.keep)
        .then(() => {
          console.log(
            `Cleared ${trgDir.info} due to previous failure.`.magenta
          );
        })
        .catch((err) => {
          console.error(
            `ERROR: Could not remove ${trgDir.info} on failure.`.error
          );
          console.error(err.message.error);
        });
    }
  }
}


// Copy files from a given src directory from the main repo into the given dst directory
//
// Example: copyFilesFromMainRepository("docs/adr", "docs/architecture/adr");
//          copyFilesFromMainRepository("docs/adr", "docs/architecture/adr", ["adr_0000.md", "adr_README.md"]);
//
// @param src     required source directory in main repository (docsConfig.repository)
// @param dst     required target directory in this repository relative to config.targetPath
// @param exclude optional array of files to exclude from srcPath
// @param keep    optional array of files to keep in dstPath
async function copyFilesFromMainRepository(srcPath, dstPath, exclude, keep) {
  exclude = exclude || [];
  keep = keep || [];

  if (fs.existsSync(srcPath)) {
    console.error(`${srcPath.info}.`.error);
  }

  if (fs.existsSync(dstPath)) {
    await removeExistingMarkdownFilesFromDirectory(dstPath, keep);
  } else {
    fs.mkdirSync(dstPath);
    console.info(`Create target directory ${dstPath.info}...`.success);
  }

  fs.readdirSync(srcPath).map((fileName) => {
    if (!exclude.includes(fileName)) {
      console.log(`Copy ${fileName.info} to ${dstPath.info}...`.success);

      fs.copyFileSync(`${srcPath}/${fileName}`, `${dstPath}/${fileName}`);
    }
  });
}

async function removeExistingMarkdownFilesFromDirectory(dirPath, filesToKeep) {
  console.info(`Remove existing markdown files from ${dirPath.info}`)
  const allFiles = await readDirectory(dirPath, false);
  allFiles
    .filter((fileName) => fileName.endsWith(".md"))
    .filter(fileName => doNotKeepFile(fileName, filesToKeep))
    .forEach((fileName) => {
      const filePath = `${dirPath}/${fileName}`;
      rimraf.sync(filePath);
      console.warn(`WARN: ${filePath} was deleted.`.warn);
    });
}

function doNotKeepFile(fileName, filesToKeep) {
  // Helper method to make it harder to oversee the !. It is easier to see the negation in the name instead
  // somewhere in the used filter invocation.
  return !keepFile(fileName, filesToKeep);
}

function keepFile(fileName, filesToKeep) {
  console.info(`Determine whether to keep '${fileName}' (${filesToKeep})`.info);

  for (let index in filesToKeep) {
    const fileToKeep = filesToKeep[index];

    if (fileName.normalize() === fileToKeep.normalize()) {
      console.info(`Keeping file ${fileName}`.info);
      return true;
    }
  }

  return false;
}