# -*- coding: utf-8 -*-
# Copyright 2018 Google Inc.
#
# 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
#
#      http://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.
"""Task for analysing Jenkins."""

from __future__ import unicode_literals

import os
import re

from turbinia import TurbiniaException
from turbinia.evidence import ReportText
from turbinia.lib import text_formatter as fmt
from turbinia.workers import TurbiniaTask
from turbinia.workers import Priority
from turbinia.lib.utils import extract_files
from turbinia.lib.utils import bruteforce_password_hashes


class JenkinsAnalysisTask(TurbiniaTask):
  """Task to analyze a Jenkins install."""

  def run(self, evidence, result):
    """Run the Jenkins worker.

    Args:
        evidence (Evidence object):  The evidence to process
        result (TurbiniaTaskResult): The object to place task results into.

    Returns:
        TurbiniaTaskResult object.
    """
    # What type of evidence we should output.
    output_evidence = ReportText()

    # Where to store the resulting output file.
    output_file_name = 'jenkins_analysis.txt'
    output_file_path = os.path.join(self.output_dir, output_file_name)

    # Set the output file as the data source for the output evidence.
    output_evidence.local_path = output_file_path

    # TODO(aarontp): We should find a more optimal solution for this because
    # this requires traversing the entire filesystem and extracting more files
    # than we need.  Tracked in https://github.com/google/turbinia/issues/402
    try:
      collected_artifacts = extract_files(
          file_name='config.xml', disk_path=evidence.local_path,
          output_dir=os.path.join(self.output_dir, 'artifacts'))
    except TurbiniaException as e:
      result.close(self, success=False, status=str(e))
      return result

    jenkins_artifacts = []
    jenkins_re = re.compile(r'^.*jenkins[^\/]*(\/users\/[^\/]+)*\/config\.xml$')
    for collected_artifact in collected_artifacts:
      if re.match(jenkins_re, collected_artifact):
        jenkins_artifacts.append(collected_artifact)

    version = None
    credentials = []
    for filepath in jenkins_artifacts:
      with open(filepath, 'r') as input_file:
        config = input_file.read()

      extracted_version = self._extract_jenkins_version(config)
      extracted_credentials = self._extract_jenkins_credentials(config)

      if extracted_version:
        version = extracted_version

      credentials.extend(extracted_credentials)

    (report, priority, summary) = self.analyze_jenkins(version, credentials)
    output_evidence.text_data = report
    result.report_data = report
    result.report_priority = priority

    # Write the report to the output file.
    with open(output_file_path, 'w') as fh:
      fh.write(output_evidence.text_data.encode('utf8'))
      fh.write('\n'.encode('utf8'))

    # Add the resulting evidence to the result object.
    result.add_evidence(output_evidence, evidence.config)
    result.close(self, success=True, status=summary)

    return result

  @staticmethod
  def _extract_jenkins_version(config):
    """Extract version from Jenkins configuration files.

    Args:
      config (str): configuration file content.

    Returns:
      str: The version of Jenkins.
    """
    version = None
    version_re = re.compile('<version>(.*)</version>')
    version_match = re.search(version_re, config)

    if version_match:
      version = version_match.group(1)

    return version

  @staticmethod
  def _extract_jenkins_credentials(config):
    """Extract credentials from Jenkins configuration files.

    Args:
      config (str): configuration file content.

    Returns:
      list: of tuples with username and password hash.
    """
    credentials = []
    password_hash_re = re.compile('<passwordHash>#jbcrypt:(.*)</passwordHash>')
    username_re = re.compile('<fullName>(.*)</fullName>')

    password_hash_match = re.search(password_hash_re, config)
    username_match = re.search(username_re, config)

    if username_match and password_hash_match:
      username = username_match.group(1)
      password_hash = password_hash_match.group(1)
      credentials.append((username, password_hash))

    return credentials

  @staticmethod
  def analyze_jenkins(version, credentials):
    """Analyses a Jenkins configuration.

    Args:
      version (str): Version of Jenkins.
      credentials (list): of tuples with username and password hash.

    Returns:
      Tuple(
        report_text(str): The report data
        report_priority(int): The priority of the report (0 - 100)
        summary(str): A summary of the report (used for task status)
      )
    """
    report = []
    summary = ''
    priority = Priority.LOW
    credentials_registry = {hash: username for username, hash in credentials}
    # TODO: Add timeout parameter when dynamic configuration is ready.
    # Ref: https://github.com/google/turbinia/issues/244
    weak_passwords = bruteforce_password_hashes(credentials_registry.keys())

    if not version:
      version = 'Unknown'
    report.append(fmt.bullet('Jenkins version: {0:s}'.format(version)))

    if weak_passwords:
      priority = Priority.CRITICAL
      summary = 'Jenkins analysis found potential issues'
      report.insert(0, fmt.heading4(fmt.bold(summary)))
      line = '{0:n} weak password(s) found:'.format(len(weak_passwords))
      report.append(fmt.bullet(fmt.bold(line)))
      for password_hash, plaintext in weak_passwords:
        line = 'User "{0:s}" with password "{1:s}"'.format(
            credentials_registry.get(password_hash), plaintext)
        report.append(fmt.bullet(line, level=2))
    elif credentials_registry or version != 'Unknown':
      summary = (
          'Jenkins version {0:s} found with {1:d} credentials, but no issues '
          'detected'.format(version, len(credentials_registry)))
      report.insert(0, fmt.heading4(summary))
      priority = Priority.MEDIUM
    else:
      summary = 'No Jenkins instance found'
      report.insert(0, fmt.heading4(summary))

    report = '\n'.join(report)
    return (report, priority, summary)
