secureCodeBox/secureCodeBox

View on GitHub
scanners/sslyze/parser/parser.js

Summary

Maintainability
B
5 hrs
Test Coverage
A
96%
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

function parse(fileContent) {
  // Only 0 when the target wasn't reachable
  if (!fileContent.server_scan_results || fileContent.server_scan_results.length === 0) {
    return [];
  }

  const serverScanResult = fileContent.server_scan_results[0];
  
  if (serverScanResult.connectivity_status == "ERROR"){
    console.error(
      "Cannot parse the result file, as some of the scan parts failed."
    );
    return [];
  }

  if (process.env["DEBUG"] === "true") {
    console.log("Parsing Result File");
    console.log(JSON.stringify(fileContent));
  }

  if (fileContent.date_scans_completed) {
    // I ran into an issue where the time coverted to ISO String was dependant from the timezone of the machine running the test. 
    // This means that if GitHub Actions CI time and local time are different the test will fail.
    // To fix this we need to enforce the timezone in the date string. 
    // sslyze uses UTC time internally for the date_scans_completed field.
    // https://github.com/nabla-c0d3/sslyze/blob/8ad73ec3d698c826bf3682aacbee2d91e4a2cdbc/sslyze/__main__.py#L83
    // To enforce UTC time, we can just add a Z to the end of the date string.
    serverScanResult.identified_at = new Date(fileContent.date_scans_completed+ "Z").toISOString();
  }

  const partialFindings = [
    generateInformationalServiceFinding(serverScanResult),
    ...generateVulnerableTLSVersionFindings(serverScanResult),
    ...analyseCertificateDeployments(serverScanResult),
  ];

  const { ip_address, hostname, port } = serverScanResult.server_location;
  const location = `${hostname || ip_address}:${port}`;

  // Enhance partialFindings with common properties shared across all SSLyze findings
  const findings = partialFindings.map((partialFinding) => {
    return {
      osi_layer: "PRESENTATION",
      reference: null,
      location,
      ...partialFinding,
      attributes: {
        hostname,
        ip_addresses: [ip_address],
        port,
        ...(partialFinding.attributes || {}),
      },
    };
  });

  return findings;
}

module.exports.parse = parse;

// Returns the Scan Result for the individual TLS Versions as array
function getTlsScanResultsAsArray(serverScanResult) {
  const commandResult = serverScanResult.scan_result;
  return [
    { name: "SSL 2.0", ...commandResult.ssl_2_0_cipher_suites.result },
    { name: "SSL 3.0", ...commandResult.ssl_3_0_cipher_suites.result },
    { name: "TLS 1.0", ...commandResult.tls_1_0_cipher_suites.result },
    { name: "TLS 1.1", ...commandResult.tls_1_1_cipher_suites.result },
    { name: "TLS 1.2", ...commandResult.tls_1_2_cipher_suites.result },
    { name: "TLS 1.3", ...commandResult.tls_1_3_cipher_suites.result },
  ];
}

// Returns all supported cipher suites across all tls and ssl version as one big string array
function getAllAcceptedCipherSuites(serverScanResult) {
  const tlsScanResults = getTlsScanResultsAsArray(serverScanResult);

  // Use set to eliminate duplicates automatically
  const supportedVersions = new Set();

  for (const tlsScanResult of tlsScanResults) {
    for (const acceptedCipherSuit of tlsScanResult.accepted_cipher_suites ||
      []) {
      supportedVersions.add(acceptedCipherSuit.cipher_suite.openssl_name);
    }
  }

  // return set as a array
  return [...supportedVersions.values()];
}

// Returns all supported tls versions as a string array
function getAllSupportedTlsVersions(serverScanResult) {
  const tlsScanResults = getTlsScanResultsAsArray(serverScanResult);

  const supportedVersions = [];

  for (const tlsScanResult of tlsScanResults) {
    // Should have at least one accepted cipher suite to be considered "supported"
    if (
      tlsScanResult.accepted_cipher_suites &&
      tlsScanResult.accepted_cipher_suites.length > 0
    ) {
      supportedVersions.push(tlsScanResult.name);
    }
  }

  return supportedVersions;
}

function generateInformationalServiceFinding(serverScanResult) {
  return {
    name: "TLS Service",
    description: "",
    identified_at: serverScanResult.identified_at,
    category: "TLS Service Info",
    severity: "INFORMATIONAL",
    mitigation: null,
    attributes: {
      tls_versions: getAllSupportedTlsVersions(serverScanResult),
      cipher_suites: getAllAcceptedCipherSuites(serverScanResult),
    },
  };
}

function generateVulnerableTLSVersionFindings(serverScanResult) {
  const supportedTlsVersions = getAllSupportedTlsVersions(serverScanResult);

  const DEPRECATED_VERSIONS = ["SSL 2.0", "SSL 3.0", "TLS 1.0", "TLS 1.1"];

  const findings = supportedTlsVersions
    .filter((tlsVersion) => DEPRECATED_VERSIONS.includes(tlsVersion))
    .map((tlsVersion) => {
      return {
        name: `TLS Version ${tlsVersion} is considered insecure`,
        category: "Outdated TLS Version",
        description: "The server uses outdated or insecure tls versions.",
        identified_at: serverScanResult.identified_at,
        severity: "MEDIUM",
        mitigation: "Upgrade to a higher tls version.",
        attributes: {
          outdated_version: tlsVersion,
        },
      };
    });

  return findings;
}

function analyseCertificateDeployments(serverScanResult) {
  if (serverScanResult?.scan_result?.certificate_info?.result?.certificate_deployments) {
    const certificateInfos = serverScanResult.scan_result.certificate_info.result.certificate_deployments.map(
      analyseCertificateDeployment
    );
    // If at least one cert is totally trusted no finding should be created
    if (certificateInfos.every((certInfo) => certInfo.trusted)) {
      return [];
    }

    // No Cert Deployment is trusted, creating individual findings

    const findingTemplates = [];
    for (const certInfo of certificateInfos) {
      if (certInfo.matchesHostname === false) {
        findingTemplates.push({
          name: "Invalid Hostname",
          description:
            "Hostname of Server didn't match the certificates subject names",
        });
      } else if (certInfo.selfSigned === true) {
        findingTemplates.push({
          name: "Self-Signed Certificate",
          description: "Certificate is self-signed",
        });
      } else if (certInfo.expired === true) {
        findingTemplates.push({
          name: "Expired Certificate",
          description: "Certificate has expired",
        });
      } else if (certInfo.untrustedRoot === true) {
        findingTemplates.push({
          name: "Untrusted Certificate Root",
          description:
            "The certificate chain contains a certificate not trusted ",
        });
      }
    }

    return findingTemplates.map((findingTemplate) => {
      return {
        name: findingTemplate.name,
        category: "Invalid Certificate",
        description: findingTemplate.description,
        identified_at: serverScanResult.identified_at,
        severity: "MEDIUM",
        mitigation: null,
        attributes: {},
      };
    });
  } else {
    // No certificate info found
    return [{
      name: "ASN.1 Parsing Error",
      category: "Invalid Certificate",
      description: "An error occurred while parsing the ASN.1 value in the certificate. This may be due to a corrupted certificate, improper formatting, or incompatibility with the cryptography library.",
      identified_at: serverScanResult.identified_at,
      severity: "MEDIUM",
      mitigation: "Verify the integrity of the certificate, or inspect the certificate for custom or non-standard extensions.",
      attributes: {},
    }
  ];
  }
}

function analyseCertificateDeployment(certificateDeployment) {
  const errorsAcrossAllTruststores = new Set();

  for (const {
    openssl_error_string,
  } of certificateDeployment.path_validation_results) {
    if (openssl_error_string !== null) {
      errorsAcrossAllTruststores.add(openssl_error_string);
    }
  }

  const matchesHostname =
    certificateDeployment.leaf_certificate_subject_matches_hostname;

  return {
    // To be trusted no openssl errors should have occurred and should match hostname
    trusted: errorsAcrossAllTruststores.size === 0 && matchesHostname,
    matchesHostname,
    selfSigned: errorsAcrossAllTruststores.has("self signed certificate"),
    expired: errorsAcrossAllTruststores.has("certificate has expired"),
    untrustedRoot: errorsAcrossAllTruststores.has(
      "self signed certificate in certificate chain"
    ),
  };
}