Similarity Uniform Fuzzy Hash Save

Similarity algorithm (computes the similarity between two files as a 0 to 1 score) with linear complexity, based on context triggered piecewise (fuzzy) hashes.

Project README

Similarity Uniform Fuzzy Hash

Similarity Uniform Fuzzy Hash is a tool that allows to accurately and efficiently compute the similarity between two files (or sets of bytes) as a 0 to 1 score.

For that purpose, it first computes for each file a Context Triggered Piecewise Hash (CTPH), also known as fuzzy hash, and then compares the hashes.

Both, the hash computation and the hashes comparison algorithms present linear complexity, the former with respect to the file size (or the amount of bytes), and the latter with respect to the hashes length, which is proportional to the files size divided by a choosable factor. This fact makes the tool very efficient and ideal for clustering (finding the most or least similar files to a given one between a set or database of many files). In fact, there is no need to store the files, storing the hashes is enough.

The tool provides methods to:

  • Compute a file hash.

  • Compute the hashes of a set of files.

  • Compute the similarity between two hashes.

  • Compute and show in a table the similarity between a hash and a set of hashes, ordering them by similarity to the first one.

  • Compute and show in a table the similarity between all the hashes in a set.

  • Save and load hashes into / from a text file.

  • Visually compare two files or hashes, identifying their common parts.

The tool is a Java JAR and can be used in two ways:

  • By means of the command line interface.

  • As a library or dependency that can be imported into a Java project.

The latest release is available here:

https://github.com/s3curitybug/similarity-uniform-fuzzy-hash/releases/latest

Readme contents:

The Algorithm

The hash computation algorithm divides the file in blocks. The location of the divisions depends on the file contents. Thus, the blocks size is not constant, but the mean block size is chosen by the user through a parameter called "factor". So the file is divided in blocks of size around factor. Then, each block is converted into two hexadecimal numbers, the first one representing its content and the second one representing its size. Finally, the hash is written as the factor followed by each block.

This way, two files sharing some content would produce two hashes that share some blocks. The comparison algorithm finds the blocks of the first hash which are present in the second one (independently on their position), and returns a 0 to 1 similarity score based on the sum of their size, divided by the file total size, which is very accurate.

Note that the similarity score between File 1 and File 2 indicates the proportion of content of File 1 which is present in File 2. This is different to the similarity score between File 2 and File 1, which indicates the proportion of content of File 2 which is present in File 1. For files with similar size, both scores will be close. However, comparing a small file which is part of a big file to that big file would return a high score between the small file and the big one, but a low score between the big file and the small one. This means that the algorithm is able to detect small files inside big ones. For instance, it can detect images inside documents, and malwares inside executables. The tool also provides methods to compute the maximum, minimum, arithmetic mean and geometric mean between the two similarity scores of two files.

Also note that the factor must be chosen carefully. The factor indicates the mean block size, in other words, the mean amount of bytes that must appear consecutively in both files such that some similarity is added to the score. This means that choosing too small factors would divide files in too small blocks, which may lead to similarities higher than expected and false positives in similarity detections, while choosing too big factors would divide files in too big blocks, which may cause similarities lower than expected and false negatives.

Additionally, the hash length (which depends on the amount of blocks) is proportional to the file size divided by the factor. This means that big files and small factors produce large hashes (high amount of blocks), while small files and big factors produce small hashes (low amount of blocks). Consequently, it is recommended using a big factor when comparing big files, and a small factor when comparing small ones. However, two hashes can only be compared if they were computed with the same factor. This means that, when comparing small files to big ones, a small factor must be used.

Due to the hash computation algorithm nature, factor must always be an odd number and larger than 2.

Up

The Command Line Interface

In order to use the command line interface, there is no need to download or compile the project, downloading the JAR is enough.

The JAR can be executed using the following command:

java -jar similarity-uniform-fuzzy-hash-{version}.jar

A Java JRE installation is required to run the JAR.

Running the JAR without any argument or with the --help or -h argument will display the usage:

Arguments:

  • --computeFileHash or -cfh

Computes the hash of one or several files (one per argument).

The argument --factor or -f must be introduced, indicating the factor that will be used for the hash or hashes computation (remember that it must be an odd number and larger than 2).

  • --computeDirectoryHashes or -cdh

Computes the hashes of all the files inside one or several directories (one per argument).

The argument --factor or -f must be introduced, indicating the factor that will be used for the hash or hashes computation (remember that it must be an odd number and larger than 2).

The argument --recursive or -r can be introduced to indicate that directories inside directories must be traversed recursively.

  • --saveToTextFile or -stf

Saves all computed hashes into one or several text files (one per argument) in their hexadecimal representation. The hashes are appended to the end of the file.

The argument --overwrite or -o can be introduced to indicate that the file must be overwritten, instead of appending the hashes to its end.

  • --saveToAsciiFile or -saf

Saves all computed hashes into one or several text files (one per argument) in their ascii representation, which is less human readable than the hexadecimal representation, but occupies less disk space. The hashes are appended to the end of the file.

The argument --overwrite or -o can be introduced to indicate that the file must be overwritten, instead of appending the hashes to its end.

  • --loadFromTextFile or -ltf

Loads all the hashes saved in one or several text files (one per argument). All hashes must be in their hexadecimal representation. Lines starting by # are ignored.

  • --loadFromAsciiFile or -laf

Loads all the hashes saved in one or several text files (one per argument). All hashes must be in their ascii representation. Lines starting by # are ignored.

  • --exportToTextFile or -etf

Exports all the hashes saved in a text file (first argument) in their ascii representation to another text file (second argument) saving them in their hexadecimal representation. The hashes are appended to the end of the file.

The argument --overwrite or -o can be introduced to indicate that the file must be overwritten, instead of appending the hashes to its end.

  • --exportToAsciiFile or -eaf

Exports all the hashes saved in a text file (first argument) in their hexadecimal representation to another text file (second argument) saving them in their ascii representation. The hashes are appended to the end of the file.

The argument --overwrite or -o can be introduced to indicate that the file must be overwritten, instead of appending the hashes to its end.

  • --compare or -x

Compares two hashes.

-If no argument is introduced, and two hashes were computed with the argument --computeFileHash or -cfh, they are compared.

-If one argument is introduced indicating a computed or loaded hash, and another hash was computed with the argument --computeFileHash or -cfh, the computed hash is compared to the indicated one.

-If two arguments are introduced indicating computed or loaded hashes, they are compared.

  • --compareToAll or -xya

Compares in a table a hash to all computed and loaded hashes, showing in the table the direct similarity (hash to hashes), the reverse similarity (hashes to hash), the maximum and the minimum between both, and their arithmetic and geometric mean.

The argument --sortingBy or -sort can be introduced to sort the table by similarity. If no argument is introduced, the default sorting criterion will be by descending direct similarity. An argument can be introduced to specify a different criterion. Check the JAR --help or -h argument to see all the possible criteria.

The argument --rowsLimit or -limit can be introduced, indicating the maximum number of rows to display in the table.

The argument --truncateNames or -trunc can be introduced, indicating the maximum number of characters to display in the hashes names.

The argument --markAbove or -ma can be introduced, indicating an upper threshold (0 to 1) to mark all similarities above or equal to it with a color.

The argument --markBelow or -mb can be introduced, indicating a lower threshold (0 to 1) to mark all similarities below it with a color.

About the --compareToAll or -xya argument:

-If no argument is introduced, and a hash was computed with the argument --computeFileHash or -cfh, the computed hash is compared to all computed and loaded hashes.

-If one argument is introduced indicating a computed or loaded hash, it is compared to all computed and loaded hashes.

-If multiple arguments are introduced indicating computed or loaded hashes, the first one is compared to all the indicated ones.

  • --compareAll or -xa

Compares in a table all computed and loaded hashes, showing in the table for each hash its similarity to every other one.

The argument --truncateNames or -trunc can be introduced, indicating the maximum number of characters to display in the hashes names.

The argument --markAbove or -ma can be introduced, indicating an upper threshold (0 to 1) to mark all similarities above or equal to it with a color.

The argument --markBelow or -mb can be introduced, indicating a lower threshold (0 to 1) to mark all similarities below it with a color.

About the --compareToAll or -xya argument:

-If no argument is introduced, all computed and loaded hashes are compared.

-If multiple arguments are introduced indicating computed or loaded hashes, all the indicated ones are compared.

  • --representVisually or -rv

Shows a visual representation of a hash. Each block is represented as one or several characters, depending on the block size.

The argument --lineWrap or -wrap can be introduced, indicating the length at which lines will be wrapped. At the beginning of each line, a percentage will be displayed indicating the file size scroll.

About the --representVisually or -rv argument:

-If no argument is introduced, and a hash was computed with the argument --computeFileHash or -cfh, the computed hash is represented visually.

-If one argument is introduced indicating a computed or loaded hash, it is visually represented.

  • --compareVisually or -xv

Shows a visual comparison of two hashes. Each block is represented as one or several characters, depending on the block size. The blocks which are present on both hashes are marked with a different color to the ones which are only present on one of them.

The argument --lineWrap or -wrap can be introduced, indicating the length at which lines will be wrapped. At the beginning of each line, a percentage will be displayed indicating the file size scroll.

About the --compareVisually or -xv argument:

-If no argument is introduced, and two hashes were computed with the argument --computeFileHash or -cfh, they are compared visually.

-If one argument is introduced indicating a computed or loaded hash, and another hash was computed with the argument --computeFileHash or -cfh, the computed hash is compared visually to the indicated one.

-If two arguments are introduced indicating computed or loaded hashes, they are compared visually.

Up

The Java Library

There are two ways to import the Java library into another Java project:

  • As an external JAR: There is no need to download or compile the project, downloading the JAR and adding it to the project as a library is enough.

  • As a Maven dependency (it is available from the Maven central repository):

<dependency>
      <groupId>com.github.s3curitybug</groupId>
      <artifactId>similarity-uniform-fuzzy-hash</artifactId>
      <version>LATEST</version>
</dependency>

The library provides the following classes and methods:

  • UniformFuzzyHash: Represents a Uniform Fuzzy Hash.

    • [constructor]: Given a byte[] and a factor (remember that it must be an odd number and larger than 2), builds a UniformFuzzyHash. It is polymorphed to build the hash from a String, InputStream, ByteArrayOutputStream or File instead of from a byte[].

    • [static] checkFactor: Checks if a factor is valid. It must be an odd number and larger than 2.

    • toString: Returns the hexadecimal representation of this UniformFuzzyHash.

    • toAsciiString: Returns the ascii representation of this UniformFuzzyHash, which is less human readable than the hexadecimal representation, but is shorter.

    • [static] rebuildFromString: Rebuilds a UniformFuzzyHash from its hexadecimal representation.

    • [static] rebuildFromAsciiString: Rebuilds a UniformFuzzyHash from its ascii representation.

    • similarity: Computes the similarity of this UniformFuzzyHash to another one, and returns it as a 0 to 1 double.

    • reverseSimilarity: Computes the similarity of another UniformFuzzyHash to this one, and returns it as a 0 to 1 double.

    • maxSimilarity: Returns the maximum between similarity and reverseSimilarity.

    • minSimilarity: Returns the minimum between similarity and reverseSimilarity.

    • arithmeticMeanSimilarity: Returns the arithmetic mean between similarity and reverseSimilarity.

    • geometricMeanSimilarity: Returns the geometric mean (square root of the product) between similarity and reverseSimilarity.

  • UniformFuzzyHashes: Provides utility static methods related to the Uniform Fuzzy Hash usage.

    • computeHashesFromByteArrays: Given a Collection of byte[] and a factor (remember that it must be an odd number and larger than 2), computes and returns a Collection of UniformFuzzyHashes. The following methods are equivalent, but receive a Collection of Strings, InputStreams, ByteArrayOutputStreams or Files instead of a Collection of byte[]: computeHashesFromStrings, computeHashesFromInputStreams, computeHashesFromByteArrayOutputStreams, computeHashesFromFiles (allows recursive traversing of Files that represent a directory).

    • computeNamedHashesFromNamedByteArrays: Given a Map relating names to byte[] and a factor (remember that it must be an odd number and larger than 2), computes and returns a Map relating names to UniformFuzzyHashes. The following methods are equivalent, but receive a Map relating names to Strings, InputStreams, ByteArrayOutputStreams or Files instead of a Map relating names to byte[]: computeNamedHashesFromNamedStrings, computeNamedHashesFromNamedInputStreams, computeNamedHashesFromNamedByteArrayOutputStreams, computeNamedHashesFromNamedFiles.

    • computeNamedHashesFromFiles: Given a Collection of Files and a factor (remember that it must be an odd number and larger than 2), computes and returns a Map relating each File name to the File UniformFuzzyHash. Allows recursive traversing of Files that represent a directory.

    • computeHashesFromDirectoryFiles: Given a directory and a factor (remember that it must be an odd number and larger than 2), computes and returns a Collection of the UniformFuzzyHashes of the Files inside the directory. Allows recursive traversing of Files that represent a directory.

    • computeNamedHashesFromDirectoryFiles: Given a directory and a factor (remember that it must be an odd number and larger than 2), computes and returns a Map relating each the name of each File inside the directory to the File UniformFuzzyHash. Allows recursive traversing of Files that represent a directory.

    • hashesToStrings: Given a Collection of UniformFuzzyHashes, returns a Collection of Strings with their hexadecimal representations. The method hashesToAsciiStrings is equivalent, with the ascii representations instead of the hexadecimal ones.

    • namedHashesToNamedStrings: Given a Map relating names to UniformFuzzyHashes, returns a Map relating names to Strings with their hexadecimal representations. The method namedHashesToNamedAsciiStrings is equivalent, with the ascii representations instead of the hexadecimal ones.

    • namedHashesToTextLines: Given a Map relating names to UniformFuzzyHashes, returns a Collection of Strings with their names and hexadecimal representations. The method namedHashesToAsciiLines is equivalent, with the ascii representations instead of the hexadecimal ones.

    • rebuildHashesFromStrings: Given a Collection of Strings with the hexadecimal representations of UniformFuzzyHashes, returns the Collection of rebuilt UniformFuzzyHashes. The method rebuildHashesFromAsciiStrings is equivalent, with the ascii representations instead of the hexadecimal ones.

    • rebuildNamedHashesFromNamedStrings: Given a Map relating names to Strings with the hexadecimal representations of UniformFuzzyHashes, returns the Map relating the names to the rebuilt UniformFuzzyHashes. The method rebuildHashesFromAsciiStrings is equivalent, with the ascii representations instead of the hexadecimal ones.

    • rebuildNamedHashesFromTextLines: Given a Collection of Strings with the names and hexadecimal representations of UniformFuzzyHashes, returns the Map relating the names to the rebuilt UniformFuzzyHashes. The method rebuildNamedHashesFromAsciiLines is equivalent, with the ascii representations instead of the hexadecimal ones.

    • saveToTextFile: Saves a Map relating names to UniformFuzzyHashes into a File, in their hexadecimal representation (one name and its UniformFuzzyHash hexadecimal representation per line). The method saveToAsciiFile is equivalent, with the ascii representations instead of the hexadecimal ones.

    • loadFromTextFile: Loads a Map relating names to UniformFuzzyHashes from a File storing them in their hexadecimal representation (one name and its UniformFuzzyHash hexadecimal representation per line). The method loadFromAsciiFile is equivalent, with the ascii representations instead of the hexadecimal ones.

    • sortBySimilarity: Sorts a Collection of UniformFuzzyHashes or a Map relating names to UniformFuzzyHashes (polymorphed) by their similarity to another UniformFuzzyHash. They can be sorted by ascending or descending similarity, reverseSimilarity, maxSimilarity, minSimilarity, arithmeticMeanSimilarity or geometricMeanSimilarity.

    • printHashes: Prints a Collection of UniformFuzzyHashes or a Map relating names to UniformFuzzyHashes (polymorphed), using their hexadecimal representation.

    • printHashesTable: Given a Collection of UniformFuzzyHashes or a Map relating names to UniformFuzzyHashes (polymorphed), prints a table showing their statistics (factor, data size, number of blocks, block size mean and block size standard deviation) and hexadecimal representations.

    • printHashToHashesSimilaritiesTable: Given a UniformFuzzyHash and a Collection of UniformFuzzyHashes or a Map relating names to UniformFuzzyHashes (polymorphed), prints a table showing the similarity, reverseSimilarity, maxSimilarity, minSimilarity, arithmeticMeanSimilarity and geometricMeanSimilarity between the hash and the hashes. The table can be sorted by any of the similarities, ascending or descending. The number of rows can be limited, the hashes names can be truncated, and it is possible to mark with a color the similarities that are above or below a threshold.

    • printAllHashesSimilaritiesTable: Given a Collection of UniformFuzzyHashes or a Map relating names to UniformFuzzyHashes (polymorphed), prints a table showing for each hash its similarity to every other one. The hashes names can be truncated, and it is possible to mark with a color the similarities that are above or below a threshold.

  • VisualRepresentation: Provides utility static methods to represent and compare Uniform Fuzzy Hashes in a visual way.

    • represent: Returns a String representing a UniformFuzzyHash in a visual way. Each block is represented as one or several characters, depending on the block size. The characters base and the number of characters per factor size can be chosen.

    • print: Prints a String representing a UniformFuzzyHash in a visual way, wrapping it at a choosable length. It is possible to print at the beginning of each wrapped line, a percentage indicating the wrap scroll.

    • representCompared: Returns a String representing a UniformFuzzyHash in a visual way like the represent method, but coloring the blocks which are present in another Uniform Fuzzy Hash with a different color to the ones which are not.

    • printCompared: Prints two Strings representing two UniformFuzzyHashes in a visual way like the print method, but coloring the blocks which are in both hashes with a different color to the ones which are only present on one of them.

Up

The Java Project

The Java project can be downloaded from GitHub:

https://github.com/s3curitybug/similarity-uniform-fuzzy-hash/archive/master.zip

It is a typical JAR Maven Java project:

  • src/main/java: Contains the source code. Packages:

    • com.github.s3curitybug.similarityuniformfuzzyhash: Contains all the Similarity Uniform Fuzzy Hash classes:

      • UniformFuzzyHash: Represents a Uniform Fuzzy Hash.
      • UniformFuzzyHashBlock: Represent a Block of a Uniform Fuzzy Hash.
      • UniformFuzzyHashes: Provides utility static methods related to the Uniform Fuzzy Hash usage.
      • VisualRepresentation: Provides utility static methods to represent and compare Uniform Fuzzy Hashes in a visual way.
      • ToStringUtils: Provides utility methods and constants to build string representations of Uniform Fuzzy Hashes.
    • org.apache.commons.cli: Contains a modification of the Apache Commons Cli library:

      • HelpFormatter: A formatter of help messages for command line options.
  • src/main/resources: Contains resources used by the source code:

    • VisualPrint: Contains the resources used by the VisualRepresentation class to represent Uniform Fuzzy Hashes in a visual way:

      • printableAscii.base: Default base of characters used to represent Uniform Fuzzy Hashes Blocks in a visual way. It is composed by printable ascii characters.
  • src/test/java: Contains the test code. Packages:

    • com.github.s3curitybug.similarityuniformfuzzyhash: Contains all the Similarity Uniform Fuzzy Hash test classes:

      • UniformFuzzyHashTest: Contains JUnit methods to test the UniformFuzzyHash class.
      • UniformFuzzHashesTest: Contains JUnit methods to test the UniformFuzzyHashes class.
      • VisualRepresentationTest: Contains JUnit methods to test the VisualRepresentation class.
      • TestResoucesUtils: Provides utility methods and constants to use resources in tests.
  • src/test/resources: Contains resources used by the test code. See the Tests section.

  • doc: Contains the Javadoc.

Up

Tests

The src/test/resources folder contains files to test the algorithm:

  • LoremIpsum: Contains 4 text files, each one with some Lorem Ipsum paragraphs:

Comparing them all:

File A is completely contained inside files ABCD and AE, and its similarity score to them is close to 1, while its similarity score to E is 0.

Around 1/4 of file ABCD is contained in files A and AE, and its similarity score to them is close to 0.25, while its similarity score to E is 0.

Around 1/2 of file AE is contained in files A, ABCD and E, and its similarity score to them is close to 0.5.

File E is completely contained inside file AE, and its similarity score to it is close to 1, while its similarity score to A and ABCD is 0.

  • RandomText/RandomText1: Contains 7 text files, each one with some random strings of 1000 characters. Comparing them all:

  • RandomText/RandomText2: Contains 5 text files:

    • File B contains a random string of 1000 characters.
    • File B' contains the same string with 1 character modified.
    • File B- contains the same string with 1 character deleted.
    • File B+ contains the same string with 1 character added.
    • File B~ contains the same string with 2 characters swapped.

This test checks that the algorithm works on modifications, deletions, additions and swaps. Comparing all the files:

  • Images: Contains 3 BMP images:

Comparing them all:

Image1 and Image2 have a similarity score of 0.919, while their similarity score to Image3 is 0. So image matching is possible.

  • InsideDoc: Contains a PNG image and 4 DOCX documents.

    • Lenna.png is the image.
    • Doc_Lenna.docx is a document containing Lenna.png, some text paragraphs and another image.
    • Doc_Lenna_Big.docx is a document containing Lenna.png, and a lot of text paragraphs and images.
    • Doc_Lenna_Swap.docx is a document containing first another image and then Lenna.png, and also some text paragraphs.
    • Doc_No_Lenna.png is a document containing an image which is not Lenna.png, and some text paragraphs.

This test checks that the algorithm is able to detect objects inside bigger files. Comparing Lenna.png to all the files:

Lenna.png is detected inside Doc_Lenna.docx, Doc_Lenna_Big.docx and Doc_Lenna_Swap.docx, and not detected inside Doc_No_Lenna.docx. So objects inside bigger files detection is possible.

Comparing visually Lenna.png to Doc_Lenna.docx:

Comparing visually Lenna.png to Doc_Lenna_Swap.docx:

  • InsidePdf: This test is similar to the InsideDoc one, but using PDF documents instead of DOCX. The image has been converted to multiple formats: BMP, GIF, JPG, and the original PNG. Comparing all the files:

The image is not detected in any format inside any document. However, if the PDF object that stores the image is extracted to a file (Lenna.pdfobj), it is detected inside Doc_Lenna.pdf, Doc_Lenna_Big.pdf and Doc_Lenna_Swap.pdf, and not detected inside Doc_No_Lenna.pdf. So image detection inside PDF is possible.

This test checks that the algorithm is able to detect web pages that come from the same site. Comparing all the files:

Each HTML matches all the HTMLs coming from its site, and none coming from a different one. So HTML coming a site detection is possible.

  • Malware: Contains 15 executable malwares. This directory is ignored from version control.

5 of the malwares are classified by Kaspersky as Trojan.Win32.Neurevt.yvd:

y_3f49584409b1ca9f14fc1e38edcf66c7ae6d2fbc2fa9e2191de021a32057466c y_6d3e33e092011c2989338c0bebd5230ab9aab21971b1e99da350280c24d56b27 y_065c42c986840781f67378672c49b4521d49f7c0711db83409ae4554f772f856 y_75f979a5f2c72cef009914adb831d00ece96a4e76eb9c95a0723927e5b7305b4 y_9721b58b885685e2ed906b4cb52f07218c9c0450fd4b255cef382cee8c19f1fd

5 of them as Trojan.Win32.Pincav.cjwu:

c_7c9b4933ea539e62d5d7a5077570d9700f056fff5c4a8ff0af0aa03bcef45d8e c_7f01fcc038c16dd09eef29a1f91631c23e7cbe8d51e962ed59f642dbc64876a2 c_81fdd8906766b4c34825118b75a213a1791cae59e32f0ca3a5ccddb5ed75cdac c_663fad07947d03da449ae188393ce00431b0fe006d8d9911f06fa95eab866301 c_c0c883e993713f55b509dd318f33aaef156cd6dc1f27f65cec2b204c6d3be0fd

5 of them as Trojan.Win32.Pincav.dxpz:

d_0dbd1808fad49f3217f1bbb624fb3bc4775f0e046fb462328072edc7186cb9e7 d_4cdba6e0536e2765b1dc562511c98838ab1ae556480859bb56302179ff6d97a4 d_8e4770ea7aa073826567fe0f9457b6c144b6357e43074794a62ed7dfd5ab0f36 d_3510f640c1f90a003925a5f192f937e85ad27443ae935ddc03852126b58b5eba d_bcc8bcfac7bc9afa064a3865c4d93c0b6bbb3afe3a1f479311fc6c0e788aeac1

This test checks that the algorithm is able to detect malwares from the same subfamily. Comparing all the files:

Each malware matches several (or, in many cases, all) malwares from its subfamily, and none from a different one. So malware subfamily detection is possible.

Up

Open Source Agenda is not affiliated with "Similarity Uniform Fuzzy Hash" Project. README Source: s3curitybug/similarity-uniform-fuzzy-hash
Stars
33
Open Issues
0
Last Commit
6 years ago
License

Open Source Agenda Badge

Open Source Agenda Rating