#69 Add number of reports on `/analysis` endpoint
Merged 7 years ago by davidcarlos. Opened 7 years ago by vbertulucci.
kiskadeemes/kiskadee 64-reports-counter  into  master

file modified
+10 -5
@@ -7,6 +7,7 @@ 

  from kiskadee.model import Package, Fetcher, Version, Analysis

  from kiskadee.api.serializers import PackageSchema, FetcherSchema,\

          AnalysisSchema

+ import json

  

  kiskadee = Flask(__name__)

  
@@ -51,12 +52,16 @@ 

              )

          analysis = (

                  db_session.query(Analysis)

-                 .filter(Analysis.version_id == version_id).first()

+                 .filter(Analysis.version_id == version_id).all()

              )

- 

-         analysis_schema = AnalysisSchema()

-         result = analysis_schema.dump(analysis)

-         return jsonify({'analysis': result.data})

+         analysis_schema = AnalysisSchema(many=True)

+         results = analysis_schema.dump(analysis)

+         for result in results.data:

+             report = result['report']

+             if (report is not None) and ('results' in report.keys()):

+                 report['results'] = json\

+                     .loads(report['results'])

+         return jsonify({'analysis': results.data})

  

  

  def kiskadee_db_session():

file modified
+15 -1
@@ -1,7 +1,20 @@ 

  """Provide objects to serialize the kiskadee models."""

  

  from marshmallow import Schema, fields

- from kiskadee.model import Package, Fetcher, Analysis, Version

+ from kiskadee.model import Package, Fetcher, Analysis, Version, Report

+ 

+ 

+ class ReportsSchema(Schema):

+     """Provide a serializer to the Reports model."""

+ 

+     id = fields.Int()

+     analysis_id = fields.Int()

+     results = fields.Dict()

+ 

+     def make_object(self, data):

+         """Serialize a Reports object."""

+         print('MAKING OBJECT FROM', data)

+         return Report(**data)

  

  

  class AnalysisSchema(Schema):
@@ -11,6 +24,7 @@ 

      version_id = fields.Int()

      analyzer_id = fields.Int()

      raw = fields.Dict()

+     report = fields.Nested(ReportsSchema)

  

      def make_object(self, data):

          """Serialize a Analysis object."""

file modified
+13 -1
@@ -3,7 +3,6 @@ 

  from sqlalchemy.ext.declarative import declarative_base

  from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\

                         Sequence, Unicode, ForeignKey, orm, JSON

- 

  import kiskadee

  

  Base = declarative_base()
@@ -74,6 +73,19 @@ 

      version_id = Column(Integer, ForeignKey('versions.id'), nullable=False)

      analyzer_id = Column(Integer, ForeignKey('analyzers.id'), nullable=False)

      raw = Column(JSON)

+     report = orm.relationship('Report',

+                               uselist=False, back_populates='analysis')

+ 

+ 

+ class Report(Base):

+     """Abstraction of a analysis report."""

+ 

+     __tablename__ = 'reports'

+     id = Column(Integer,

+                 Sequence('reports_id_seq', optional=True), primary_key=True)

+     analysis_id = Column(Integer, ForeignKey('analysis.id'), nullable=False)

+     results = Column(JSON)

+     analysis = orm.relationship('Analysis', back_populates='report')

  

  

  def create_analyzers(_session):

file modified
+44 -2
@@ -10,12 +10,18 @@ 

  import json

  

  import kiskadee.database

+ from kiskadee.report import CppcheckReport, FlawfinderReport

  from kiskadee.runner import Runner

  import kiskadee.queue

- from kiskadee.model import Package, Fetcher, Version

+ from kiskadee.model import Package, Fetcher, Version, Report

  

  RUNNING = True

  

+ REPORTERS = {

+     'cppcheck': CppcheckReport,

+     'flawfinder': FlawfinderReport

+ }

+ 

  

  class Monitor:

      """Provide kiskadee monitoring objects."""
@@ -143,6 +149,36 @@ 

          self.session.commit()

          return _package

  

+     def _save_reports(self, analysis, pkg, analyzer_name):

+         try:

+             results = analysis['results']

+             analyzer_report = REPORTERS[analyzer_name](results)

+             _reports = Report()

+             _reports.results = json.dumps(

+                     analyzer_report

+                     ._compute_reports(analyzer_name)

+                 )

+             _reports.analysis_id = analysis['id']

+             self.session.add(_reports)

+             self.session.commit()

+             kiskadee.logger.debug(

+                     "MONITOR: Saved analysis reports for {} package"

+                     .format(pkg["name"])

+                 )

+         except KeyError as key:

+             kiskadee.logger.debug(

+                     "ERROR: There's no reporter " +

+                     "to get reports from {} analyzer. ".format(key) +

+                     "Make shure to import or implement them."

+                 )

+         except Exception as err:

+             kiskadee.logger.debug(

+                     "MONITOR: Failed to get analysis reports to {} package"

+                     .format(pkg["name"])

+                 )

+             kiskadee.logger.debug(err)

+         return

+ 

      def _save_analysis(self, pkg, analyzer, result, version):

          _analysis = kiskadee.model.Analysis()

          try:
@@ -153,13 +189,19 @@ 

              _analysis.raw = json.loads(result)

              self.session.add(_analysis)

              self.session.commit()

+             dict_analysis = {

+                     'results': _analysis.raw['results'],

+                     'id': _analysis.id

+                 }

+             self._save_reports(dict_analysis, pkg, _analyzer.name)

              kiskadee.logger.debug(

                      "MONITOR: Saved analysis done by {} for package: {}_{}"

                      .format(analyzer, pkg["name"], pkg["version"])

                  )

+             return

          except Exception as err:

              kiskadee.logger.debug(

-                     "MONITOR: The required analyzer was" +

+                     "MONITOR: The required analyzer was " +

                      "not registered in kiskadee"

                  )

              kiskadee.logger.debug(err)

file added
+80
@@ -0,0 +1,80 @@ 

+ """Provide reporter capabilities to kiskadee analyzers.

+ 

+ Every analyzer on kiskadee has differents reports results.

+ This module came to compute and deal with this differences.

+ """

+ import kiskadee

+ from abc import ABCMeta, abstractmethod

+ 

+ 

+ class Report:

+     """Abstraction of a analyzer reporter."""

+ 

+     __metaclass__ = ABCMeta

+ 

+     def __init__(self, results):

+         """Initialize Report class."""

+         self.results = results

+ 

+     @abstractmethod

+     def _compute_reports(self, results): pass

+ 

+     @staticmethod

+     def logger_message(errors, analyzer):

+         """Logger of reporter computer methods."""

+         kiskadee.logger.debug(

+                 "WARNING: There are " +

+                 "{} registers on JSON ".format(errors) +

+                 "that was not able to convert to " +

+                 "'{}' reports.".format(analyzer)

+             )

+         return

+ 

+ 

+ class CppcheckReport(Report):

+     """Concrete reporter implementation to cppcheck analyzer."""

+ 

+     def _compute_reports(self, analyzer):

+         """Compute every report type for cppcheck analyzer."""

+         print(analyzer)

+         result_dict = {

+                 'warning': 0,

+                 'error': 0,

+                 'style': 0

+         }

+         count_error = 0

+         for result in self.results:

+             if 'severity' in list(result.keys()):

+                 result_dict[result['severity']] += 1

+             else:

+                 count_error += 1

+ 

+         if count_error > 0:

+             self.logger_message(count_error, analyzer)

+ 

+         return result_dict

+ 

+ 

+ class FlawfinderReport(Report):

+     """Concrete reporter implementation to flawfinder analyzer."""

+ 

+     def _compute_reports(self, analyzer):

+         """Compute every report type for flawfinder analyzer."""

+         result_dict = {

+                 'severity_1': 0,

+                 'severity_2': 0,

+                 'severity_3': 0,

+                 'severity_4': 0,

+                 'severity_5': 0

+         }

+         count_error = 0

+         for result in self.results:

+             if 'severity' in list(result.keys()):

+                 result_dict['severity_' + result['severity']] += 1

+             else:

+                 count_error += 1

+ 

+         if count_error > 0:

+             self.logger_message(count_error, analyzer)

+ 

+         return result_dict

file modified
+31 -5
@@ -18,10 +18,35 @@ 

          self.session = Session()

          self.app = kiskadee.api.app.kiskadee.test_client()

          kiskadee.model.create_analyzers(self.session)

+         kiskadee.model.Base.metadata.create_all(self.engine)

+         kiskadee.model.create_analyzers(self.session)

          fetcher = kiskadee.model.Fetcher(

                  name='kiskadee-fetcher', target='university'

          )

+         pkg = kiskadee.model.Package(

+                 name='kiskadee-package', fetcher_id=1

+         )

+         version = kiskadee.model.Version(

+                 number='7.23', package_id=1

+         )

+         analysis = kiskadee.model.Analysis(

+                 version_id=1, analyzer_id=1,

+                 raw={

+                     'results': [

+                         {'severity': 'warning'},

+                         {'severity': 'style'},

+                         {'severity': 'error'}

+                     ]

+                 }

+         )

+         report = kiskadee.model.Report(

+             analysis_id=1

+         )

          self.session.add(fetcher)

+         self.session.add(pkg)

+         self.session.add(version)

+         self.session.add(analysis)

+         self.session.add(report)

          self.session.commit()

          self.runner = Runner()

          self.monitor = Monitor(self.session)
@@ -67,11 +92,12 @@ 

          kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session

          response = self.app.get("/analysis/test/1.0.0")

          response_data = json.loads(response.data.decode("utf-8"))

-         self.assertIsNotNone(response_data["analysis"]["raw"])

-         pkg_first_analysis = response_data["analysis"]["raw"]["results"][0]

-         self.assertIn('location', pkg_first_analysis)

-         self.assertIn('cwe', pkg_first_analysis)

-         self.assertIn('message', pkg_first_analysis)

+         pkg_first_analysis = response_data["analysis"][0]

+         self.assertIsNotNone(pkg_first_analysis["raw"])

+         self.assertIn('report', pkg_first_analysis)

+         self.assertIn('location', pkg_first_analysis["raw"]["results"][0])

+         self.assertIn('cwe', pkg_first_analysis["raw"]["results"][0])

+         self.assertIn('message', pkg_first_analysis["raw"]["results"][0])

  

  

  if __name__ == '__main__':

file modified
+18 -2
@@ -15,7 +15,7 @@ 

          model.Base.metadata.create_all(self.engine)

          model.create_analyzers(self.session)

          self.fetcher = model.Fetcher(

-                 name='kiskadee-fetcher', target='university'

+               name='kiskadee-fetcher', target='university'

              )

          self.package = model.Package(name='python-kiskadee')

          self.version = model.Version(number='1.0-rc1')
@@ -27,9 +27,14 @@ 

  

          self.analysis = model.Analysis(

                  analyzer_id=1,

-                 version_id=self.version.id,

+                 version_id=1,

                  raw=""

                  )

+         self.session.add(self.analysis)

+         self.report = model.Report(

+                 analysis_id=1,

+         )

+         self.session.add(self.report)

          self.session.commit()

  

      def tearDown(self):
@@ -48,6 +53,10 @@ 

          versions = self.session.query(model.Version).all()

          self.assertEqual(versions, [self.version])

  

+     def test_query_report(self):

+         reports = self.session.query(model.Report).all()

+         self.assertEqual(reports, [self.report])

+ 

      def test_add_fetcher(self):

          fetchers = self.session.query(model.Fetcher).all()

          self.assertEqual(len(fetchers), 1)
@@ -61,6 +70,13 @@ 

          with self.assertRaises(exc.IntegrityError):

              self.session.commit()

  

+     def test_add_report_without_analysis(self):

+         report = model.Report(

+         )

+         self.session.add(report)

+         with self.assertRaises(exc.IntegrityError):

+             self.session.commit()

+ 

      def test_add_package_without_fetcher(self):

          package = model.Package(name='foo-bar')

          self.session.add(package)

@@ -4,7 +4,7 @@ 

  from kiskadee import model

  from kiskadee.monitor import Monitor

  from kiskadee.queue import packages_queue

- from kiskadee.model import Package, Fetcher, create_analyzers

+ from kiskadee.model import Package, Fetcher, create_analyzers, Report, Analysis

  import kiskadee.queue

  import kiskadee.fetchers.debian

  from kiskadee.database import Database
@@ -46,6 +46,17 @@ 

                              'cppcheck': '<>',

                              'flawfinder': '><'},

                       'fetcher_id': 1}

+         self.analysis = {

+                 'analyzer_id': 1,

+                 'id': 1,

+                 'raw': {

+                     'results': [

+                         {'severity': 'warning'},

+                         {'severity': 'style'},

+                         {'severity': 'error'}

+                     ]

+                 }

+             }

  

      def tearDown(self):

          self.session.close()
@@ -86,6 +97,42 @@ 

          self.assertEqual(len(_pkgs), 2)

          self.assertEqual(_pkgs[1].name, _pkg['name'])

  

+     def test_save_reports(self):

+         _fetcher = model.Fetcher(

+                 name='kiskadee-fetcher', target='university'

+             )

+         _pkg = model.Package(

+                 name='kiskadee-package', fetcher_id=1

+             )

+         _version = model.Version(

+                 number='1.0-rc1', package_id=1

+                 )

+         _raw = {

+             'results': [

+                 {'severity': 'warning'},

+                 {'severity': 'style'},

+                 {'severity': 'error'}

+             ]

+         }

+         _analysis = Analysis(

+                 version_id=1,

+                 analyzer_id=1,

+                 id=1,

+                 raw=_raw

+             )

+         self.session.add(_fetcher)

+         self.session.add(_pkg)

+         self.session.add(_version)

+         self.session.add(_analysis)

+         self.session.commit()

+         _dict_analysis = {

+                 'results': _raw['results'],

+                 'id': 1

+             }

+         self.monitor._save_reports(_dict_analysis, self.pkg1, 'cppcheck')

+         _report = self.monitor.session.query(Report).all()

+         self.assertEqual(len(_report), 1)

+ 

      def test_save_version(self):

          self.monitor._save_fetcher(kiskadee.fetchers.debian.Fetcher())

          self.monitor._save_analyzed_pkg(self.pkg1)

@@ -0,0 +1,50 @@ 

+ import unittest

+ 

+ from sqlalchemy.orm import sessionmaker

+ from kiskadee.report import CppcheckReport, FlawfinderReport

+ from kiskadee.database import Database

+ 

+ 

+ class ReportTestCase(unittest.TestCase):

+ 

+     def setUp(self):

+         self.engine = Database('db_test').engine

+         Session = sessionmaker(bind=self.engine)

+         self.session = Session()

+ 

+     def tearDown(self):

+         self.session.close()

+ 

+     def test_compute_cppcheck_reports(self):

+         _reports = [

+                 {'severity': 'warning'},

+                 {'severity': 'error'},

+                 {'severity': 'style'},

+                 {'some-attribute': None}

+             ]

+         _cpp_reporter = CppcheckReport(_reports)

+         result = _cpp_reporter._compute_reports('cppcheck')

+         self.assertEqual(len(result.keys()), 3)

+         self.assertEqual(result['warning'], 1)

+         self.assertEqual(result['style'], 1)

+         self.assertEqual(result['error'], 1)

+ 

+     def test_compute_flawfinder_reports(self):

+         _reports = [

+                 {'severity': '5'},

+                 {'severity': '4'},

+                 {'severity': '3'},

+                 {'some-attribute': None}

+             ]

+         _flawfinder_reporter = FlawfinderReport(_reports)

+         result = _flawfinder_reporter._compute_reports('flawfinder')

+         self.assertEqual(len(result.keys()), 5)

+         self.assertEqual(result['severity_5'], 1)

+         self.assertEqual(result['severity_4'], 1)

+         self.assertEqual(result['severity_3'], 1)

+         self.assertEqual(result['severity_2'], 0)

+         self.assertEqual(result['severity_1'], 0)

+ 

+ 

+ if __name__ == '__main__':

+     unittest.main()

Merge Description

Changed /analysis/<package>/<version> endpoint to return an array of analysis.
- In a single request, the result will be an json array that contains analysis of each analyzer that was previous configured to analyze the current package;
- On each analysis, the current json will return the number of each report type, like style, error and warning.

Observations

  • [x] Rebase with upstream master;
  • [x] Build passing;
  • [x] Add unit tests for current Merge;
  • [x] Squash commits.

rebased onto 2d5ef570220877d747c6409e5005632c8c2aa374

7 years ago

2 new commits added

  • Fixed test errors
  • Added flawfinder concrete reporter
7 years ago

rebased onto 0d641aa

7 years ago

Pull-Request has been merged by davidcarlos

7 years ago