diff --git a/CHANGELOG.md b/CHANGELOG.md index f400773..3f99c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + - Ability to define custom signatures in `~/.gitrobsignatures` ## [1.0.1] ### Fixed diff --git a/README.md b/README.md index 4e04d21..1a82d46 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,41 @@ By default, the server will listen on [localhost:9393](http://localhost:9393). T See `gitrob help server` for more options. +### Adding custom signatures + +If you want to look for files that are specific to your organisation or projects, it is easy to add custom signatures. + +When Gitrob starts it looks for a file at `~/.gitrobsignatures` which it expects to be a JSON document with signatures that follow the same structure as the main [signatures.json](signatures.json) file. Here is an example: + + [ + { + "part": "filename", + "type": "match", + "pattern": "otr.private_key", + "caption": "Pidgin OTR private key", + "description": null + } + ] + + This signature instructs Gitrob to flag files where the filename exactly matches `otr.private_key`. The caption and description are used in the web interface when displaying the findings. + +#### Signature keys + + * `part`: Can be one of: + * `path`: The complete file path + * `filename`: Only the filename + * `extension`: Only the file extension + * `type`: Can be one of: + * `match`: Simple match of part and pattern + * `regex`: Regular expression matching of part and pattern + * `pattern`: The value or regular expression to match with + * `caption`: A short description of the finding + * `description`: More detailed description if needed (set to `null` if not). + +Have a look at the main [signatures.json](signatures.json) file for more examples of signatures. + +**If you think other people can benefit from your custom signatures, please consider contributing them back to the Gitrob project by opening a Pull Request or an Issue. Thanks!** + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec gitrob` to use the gem in this directory, ignoring other installed copies of this gem. diff --git a/lib/gitrob/blob_observer.rb b/lib/gitrob/blob_observer.rb index b233985..b4b3ff7 100644 --- a/lib/gitrob/blob_observer.rb +++ b/lib/gitrob/blob_observer.rb @@ -2,6 +2,8 @@ module Gitrob class BlobObserver SIGNATURES_FILE_PATH = File.expand_path( "../../../signatures.json", __FILE__) + CUSTOM_SIGNATURES_FILE_PATH = File.join( + Dir.home, ".gitrobsignatures") REQUIRED_SIGNATURE_KEYS = %w(part type pattern caption description) ALLOWED_TYPES = %w(regex match) @@ -30,23 +32,49 @@ module Gitrob def self.load_signatures! @signatures = [] - JSON.load(File.read(SIGNATURES_FILE_PATH)).each do |signature| + signatures = JSON.load(File.read(SIGNATURES_FILE_PATH)) + validate_signatures!(signatures) + signatures.each_with_index do |signature| @signatures << Signature.new(signature) end - validate_signatures! rescue CorruptSignaturesError => e raise e rescue raise CorruptSignaturesError, "Could not parse signature file" end - def self.validate_signatures! + def self.unload_signatures + @signatures = [] + end + + def self.custom_signatures? + File.exist?(CUSTOM_SIGNATURES_FILE_PATH) + end + + def self.load_custom_signatures! + signatures = JSON.load(File.read(CUSTOM_SIGNATURES_FILE_PATH)) + validate_signatures!(signatures) + signatures.each do |signature| + @signatures << Signature.new(signature) + end + rescue CorruptSignaturesError => e + raise e + rescue + raise CorruptSignaturesError, "Could not parse signature file" + end + + def self.validate_signatures!(signatures) if !signatures.is_a?(Array) || signatures.empty? fail CorruptSignaturesError, "Signature file contains no signatures" end - signatures.each do |signature| - validate_signature!(signature) + signatures.each_with_index do |signature, index| + begin + validate_signature!(signature) + rescue CorruptSignaturesError => e + raise CorruptSignaturesError, + "Validation failed for Signature ##{index + 1}: #{e.message}" + end end end @@ -58,7 +86,7 @@ module Gitrob def self.validate_signature_keys!(signature) REQUIRED_SIGNATURE_KEYS.each do |key| - unless signature.respond_to?(key) + unless signature.key?(key) fail CorruptSignaturesError, "Missing required signature key: #{key}" end @@ -66,16 +94,16 @@ module Gitrob end def self.validate_signature_type!(signature) - unless ALLOWED_TYPES.include?(signature.type) + unless ALLOWED_TYPES.include?(signature["type"]) fail CorruptSignaturesError, - "Invalid signature type: #{signature.type}" + "Invalid signature type: #{signature['type']}" end end def self.validate_signature_part!(signature) - unless ALLOWED_PARTS.include?(signature.part) + unless ALLOWED_PARTS.include?(signature["part"]) fail CorruptSignaturesError, - "Invalid signature part: #{signature.part}" + "Invalid signature part: #{signature['part']}" end end diff --git a/lib/gitrob/cli/commands/analyze.rb b/lib/gitrob/cli/commands/analyze.rb index 089eceb..1a9d75b 100644 --- a/lib/gitrob/cli/commands/analyze.rb +++ b/lib/gitrob/cli/commands/analyze.rb @@ -40,6 +40,15 @@ module Gitrob task("Loading signatures...", true) do Gitrob::BlobObserver.load_signatures! end + + if Gitrob::BlobObserver.custom_signatures? + task("Loading custom signatures...", true) do + Gitrob::BlobObserver.load_custom_signatures! + end + info("Please consider contributing your custom signatures to the " \ + "Gitrob project.") + end + info("Loaded #{Gitrob::BlobObserver.signatures.count} signatures") end def start_web_server diff --git a/spec/lib/gitrob/blob_observer_spec.rb b/spec/lib/gitrob/blob_observer_spec.rb index 8611cd4..68682be 100644 --- a/spec/lib/gitrob/blob_observer_spec.rb +++ b/spec/lib/gitrob/blob_observer_spec.rb @@ -1248,6 +1248,70 @@ describe Gitrob::BlobObserver do File.expand_path("../../../../signatures.json", __FILE__) end + let(:custom_signatures_file_path) do + File.join(Dir.home, ".gitrobsignatures") + end + + context "when custom signatures file is present" do + it "loads custom signatures" do + described_class.unload_signatures + allow(described_class).to receive(:custom_signatures?) + .and_return(true) + expect(File).to receive(:read) + .with(custom_signatures_file_path) + .and_return(' + [ + { + "part": "filename", + "type": "match", + "pattern": "test", + "caption": "Test signature", + "description": "This is a test signature" + } + ] + ') + described_class.load_custom_signatures! + expect(described_class.signatures.count).to eq(1) + signature = described_class.signatures.first + expect(signature).to be_a(Gitrob::BlobObserver::Signature) + expect(signature.part).to eq("filename") + expect(signature.type).to eq("match") + expect(signature.pattern).to eq("test") + expect(signature.caption).to eq("Test signature") + expect(signature.description).to eq("This is a test signature") + end + + it "validates custom signatures" do + described_class.unload_signatures + allow(described_class).to receive(:custom_signatures?) + .and_return(true) + allow(File).to receive(:read) + .with(custom_signatures_file_path) + .and_return(' + [ + { + "part": "filename", + "type": "match", + "pattern": "test", + "caption": "Test signature", + "description": "This is a test signature" + } + ] + ') + expect(described_class).to receive(:validate_signatures!) + .with([ + { + "part" => "filename", + "type" => "match", + "pattern" => "test", + "caption" => "Test signature", + "description" => "This is a test signature" + } + ]) + described_class.load_custom_signatures! + end + end + context "when Signature file is empty" do it "raises CorruptSignaturesError" do expect(File).to receive(:read) @@ -1258,7 +1322,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Could not parse signature file" + "Signature file contains no signatures" ) end end @@ -1312,7 +1376,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Missing required signature key: part" + "Validation failed for Signature #1: Missing required signature key: part" ) end end @@ -1336,7 +1400,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Missing required signature key: type" + "Validation failed for Signature #1: Missing required signature key: type" ) end end @@ -1360,7 +1424,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Missing required signature key: pattern" + "Validation failed for Signature #1: Missing required signature key: pattern" ) end end @@ -1384,7 +1448,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Missing required signature key: caption" + "Validation failed for Signature #1: Missing required signature key: caption" ) end end @@ -1408,7 +1472,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Missing required signature key: description" + "Validation failed for Signature #1: Missing required signature key: description" ) end end @@ -1433,7 +1497,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Invalid signature part: what" + "Validation failed for Signature #1: Invalid signature part: what" ) end end @@ -1458,7 +1522,7 @@ describe Gitrob::BlobObserver do end .to raise_error( Gitrob::BlobObserver::CorruptSignaturesError, - "Invalid signature type: what" + "Validation failed for Signature #1: Invalid signature type: what" ) end end diff --git a/spec/lib/gitrob/cli/commands/analyze_spec.rb b/spec/lib/gitrob/cli/commands/analyze_spec.rb index fcce10c..f613ebe 100644 --- a/spec/lib/gitrob/cli/commands/analyze_spec.rb +++ b/spec/lib/gitrob/cli/commands/analyze_spec.rb @@ -32,6 +32,10 @@ describe Gitrob::CLI::Commands::Analyze do deadbabedeadbabedeadbabedeadbabedeadbabe ) ) + allow(Gitrob::BlobObserver).to receive(:custom_signatures?) + .and_return(false) + allow(Gitrob::BlobObserver).to receive(:signatures) + .and_return([]) expect_any_instance_of(described_class) .to receive(:task) @@ -41,6 +45,53 @@ describe Gitrob::CLI::Commands::Analyze do described_class.new(target, options) end + context "When custom signatures are present" do + it "loads custom signtures" do + stub_db_assessment = spy + allow(stub_db_assessment) + .to receive(:save) + allow_any_instance_of(described_class) + .to receive(:gather_owners) + allow_any_instance_of(described_class) + .to receive(:gather_repositories) + allow(Gitrob::Models::Assessment) + .to receive(:create) + .and_return(stub_db_assessment) + allow_any_instance_of(described_class) + .to receive(:analyze_repositories) + allow(Gitrob::CLI) + .to receive(:configuration) + .and_return( + "github_access_tokens" => %w( + deadbeefdeadbeefdeadbeefdeadbeefdeadbeef + deadbabedeadbabedeadbabedeadbabedeadbabe + ) + ) + allow(Gitrob::BlobObserver) + .to receive(:custom_signatures?) + .and_return(true) + allow(Gitrob::BlobObserver).to receive(:signatures) + .and_return([]) + + allow_any_instance_of(described_class) + .to receive(:task) + .with("Loading signatures...", true) + .and_yield + expect_any_instance_of(described_class) + .to receive(:task) + .with("Loading custom signatures...", true) + .and_yield + expect(Gitrob::BlobObserver).to receive(:load_custom_signatures!) + expect_any_instance_of(described_class) + .to receive(:info) + .with("Please consider contributing your custom signatures to the Gitrob project.") + expect_any_instance_of(described_class) + .to receive(:info) + .with("Loaded 0 signatures") + described_class.new(target, options) + end + end + it "gathers owners" do stub_db_assessment = spy allow(stub_db_assessment)