diff --git a/README.md b/README.md index 8ff977a..4fa9834 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,11 @@ Audit request on front-end ► Validation and hash verification of project p We believe that audits should be transparent and trustless. -## 📄 Litepaper: -Simply go to [qrucial.io/qrucial-dao](https://qrucial.io/wp-content/uploads/2022/06/QRUCIAL-DAO-Litepaper-2022.pdf) +## 📄 Litepaper and whitepaper: +[Litepaper link /to be actualized/](https://raw.githubusercontent.com/Qrucial/QRUCIAL-DAO/main/docs/QRUCIAL%20DAO%20Litepaper%202022.pdf) + +[Whitepaper link /to be actualized/](https://raw.githubusercontent.com/Qrucial/QRUCIAL-DAO/main/docs/QRUCIAL_DAO_Whitepaper.pdf) + # 📚 Wiki: QDAO wiki [can be found here](https://github.com/Qrucial/QRUCIAL-DAO/wiki). diff --git a/docker/docker_clean.sh b/docker/docker_clean.sh new file mode 100755 index 0000000..2bfd5d0 --- /dev/null +++ b/docker/docker_clean.sh @@ -0,0 +1,9 @@ +#!/bin/sh +killall qdao-node +killall qdao-exosysd +killall lar.py +#docker kill $(docker ps -q) +#docker rm $(docker ps -a -q) +#docker rmi $(docker images -q) +docker system prune +docker system prune -af diff --git a/exotools/conf/nginx.conf b/exotools/conf/nginx.conf new file mode 100644 index 0000000..32aa5ff --- /dev/null +++ b/exotools/conf/nginx.conf @@ -0,0 +1,68 @@ +user www-data; +worker_processes 2; +pid /run/nginx.pid; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + sendfile off; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + gzip_disable "msie6"; + +# limit_req_zone $binary_remote_addr zone=one:10m rate=40r/s; +# limit_req zone=one; +} + +server { + listen 80; + listen [::]:80; + server_name qrucial.io; + return 301 https://$server_name:443$request_uri; +} + +server { + limit_rate 10240k; + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name qrucial.io; + gzip on; + charset UTF-8; + + index = index.html + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_buffering off; + } + + ssl_certificate /etc/letsencrypt/live/qrucial.io/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/qrucial.io/privkey.pem; # managed by Certbot + + add_header Strict-Transport-Security "max-age=256000; includeSubDomains; preload;"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + + if ($http_user_agent ~* (wget|curl|libwww-perl|masscan|nmap|acunetix|roman|burp|arachni|urllib|burger-imperia|testproxy|semantic|nikto|nessus|sqlmap|wpscan|gobuster) ) { + return 403;} + + +} + diff --git a/exotools/configs/nginx.conf b/exotools/configs/nginx.conf new file mode 100644 index 0000000..b772224 --- /dev/null +++ b/exotools/configs/nginx.conf @@ -0,0 +1,114 @@ +# Hardened configuration for QDAO nodes +user www-data; +worker_processes 2; +worker_rlimit_nofile 8192; +pid /run/nginx.pid; + +events { + worker_connections 2048; +} + +http { + sendfile off; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + gzip on; + +# Just redir to HTTPS everitiem +server { + listen 80; + #listen [::]:80; # No IPv6 for now + server_name qrucial.io; + return 301 https://$server_name:443$request_uri; + } + +## HTTPS server proxying the React based FE +server { + limit_rate 10240k; + listen 443 ssl http2; + server_name qrucial.io; + gzip on; + charset UTF-8; + types_hash_max_size 2048; + + # Buffer policy + client_body_buffer_size 1K; + client_header_buffer_size 1k; + client_max_body_size 1k; + large_client_header_buffers 2 1k; + + # Only allow GET and POST + if ($request_method !~ ^(GET|POST)$ ) + { + return 405; + } + + index = index.html; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_buffering off; + } + + # SSL + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'HIGH:EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:!aNULL:!eNULL:!LOW:!3DES:!MD5:!SRP:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA'; + ssl_certificate /etc/letsencrypt/live/qrucial.io/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/qrucial.io/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=256000; includeSubDomains; preload;"; + add_header X-Content-Type-Options nosniff; # Dont let guessing the content types + add_header X-XSS-Protection "1; mode=block"; # Browser inbuilt XSS protection blocks loading at attack + add_header X-Frame-Options "DENY"; # Iframes are disabled + add_header Set-Cookie "Path=/; HttpOnly; Secure"; # Only allow S cookies + + # Block scanners based on user-agent (nobrainer to bypass, but logs are more beautiful) + if ($http_user_agent ~* (wget|curl|libwww-perl|masscan|nmap|acunetix|roman|burp|arachni|urllib|burger-imperia|testproxy|semantic|nikto|nessus|sqlmap|wpscan|gobuster) ) {return 403;} + + } + +## WSS proxy, so QDAO's Substrate chain can provide ws on 127.0.0.1:9944 +server { + root /var/www/html; + index index.html; + + ssl_certificate /etc/letsencrypt/live/qrucial.io/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/qrucial.io/privkey.pem; + listen 9995 ssl; + + ssl_session_cache shared:cache_nginx_SSL:1m; + ssl_session_timeout 1440m; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'HIGH:EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:!aNULL:!eNULL:!LOW:!3DES:!MD5:!SRP:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA'; + + location / { + try_files $uri $uri/ =404; + + proxy_buffering off; + proxy_pass http://127.0.0.1:9944 ; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host 127.0.0.1:9944; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + } +} \ No newline at end of file diff --git a/exotools/docker/docker_files/dockerfile b/exotools/docker/docker_files/dockerfile index bd94ee8..e874280 100644 --- a/exotools/docker/docker_files/dockerfile +++ b/exotools/docker/docker_files/dockerfile @@ -1,18 +1,17 @@ FROM rust:buster +RUN apt update +RUN apt-get install -y python3-pip graphviz xdg-utils RUN cargo install cargo-audit # This will always return success, it normally fails after update (we just need update) RUN cargo audit -db || : RUN echo "Ignore Error above. just used to sync repo" +# Prepare to run the off-chain automated audit RUN mkdir /usr/exotools/ ADD ./scripts/audit_script.sh /usr/exotools/ RUN chmod +x /usr/exotools/audit_script.sh -# Figure out a check we can do to make sure the container is running nominaly. -# for the pallets having it send a ping without failing would work, -# for exotools we dont really need one yet, as they get deployed and then shut down. -# HEALTHCHECK # ping 0.0.0.0 -ENTRYPOINT ["/usr/exotools/audit_script.sh"] -# This is a not great solution but it works +# This starts the off-chain automated auditing +ENTRYPOINT ["/usr/exotools/audit_script.sh"] \ No newline at end of file diff --git a/exotools/docker/docker_files/scripts/audit_script.sh b/exotools/docker/docker_files/scripts/audit_script.sh index f02227d..fc570fa 100755 --- a/exotools/docker/docker_files/scripts/audit_script.sh +++ b/exotools/docker/docker_files/scripts/audit_script.sh @@ -74,7 +74,10 @@ function exec_audit { elif [[ $(find "$EXTRACT_PATH" -name Cargo.toml) ]]; then TOML_FILE="$(find "$EXTRACT_PATH" -name Cargo.toml)" else - echo "Cannot Continue, neither Cargo.lock, or Cargo.toml was found" + echo "Neither Cargo.lock, or Cargo.toml was found, assuming EVM/Solidity project" + cd $EXTRACT_PATH + chmod +x ~/.local/bin/octopus_eth_evm + ~/.local/bin/octopus_eth_evm -f evm.bin exit 1 fi @@ -93,25 +96,11 @@ function exec_audit { ( cd $(dirname $LOCK_FILE) && cargo audit --json > "$REPORT_PATH""report.json" ) ( cd $(dirname $LOCK_FILE) && cargo clippy &> "$REPORT_PATH""clippy.out" ) - # cargo audit --json > "$REPORT_PATH""report.json" # better save method. (?) + - # cp or symlink, whatever is better + # cp or symlink could be an alternative cp -r "$REPORT_PATH" "$TIMESTAMP_PATH" } - - - - - - - - -exec_audit - - - - - - +exec_audit \ No newline at end of file diff --git a/exotools/lar.py b/exotools/lar.py index 22e2396..fcb15bd 100644 --- a/exotools/lar.py +++ b/exotools/lar.py @@ -75,7 +75,7 @@ def init_db(): c.execute('CREATE TABLE IF NOT EXISTS auditStates (requestor, hash text, projectUrl text, state text, autoReport text, manualReport text, topAuditor text, challenger text)') c.execute("INSERT INTO auditStates VALUES('5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy','0xa03f6ba3eb8141f0f8daee4ea016d4144f44fc4cba9e7477a4c1f041aaeb6c38', 'https://v-space.hu/s/exotestflipper.tar', 'In progress', 'NA', 'NA', 'NA', 'No challenger')") c.execute("INSERT INTO auditStates VALUES('5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw','0x3e2d46f07bb14bab9d623e426246ee8115f2669fa04745e51a00e18446e47df7', 'https://v-space.hu/s/exotest.tar', 'Finished', 'Submitted', 'Submitted', 'Charlie', 'No challenger')") - c.execute("INSERT INTO auditStates VALUES('5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw','0x3e2d46f07bb14bab9d623e426246ee8115f2669fa04745e51a00e18446e47df7', 'https://v-space.hu/s/exosol.tar', 'Finished', 'Submitted', 'Submitted', 'Charlie', 'No challenger')") + c.execute("INSERT INTO auditStates VALUES('5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw','0xxx2d46f07bb14bab9d623e426246ee8115f2669fa04745e51a00e18446e47dxx', 'https://v-space.hu/s/exosol.tar', 'Finished', 'Submitted', 'Submitted', 'Charlie', 'No challenger')") conn.commit() conn.close() init_db() @@ -164,7 +164,7 @@ def index(): # ExoTool calls this, letting lar.py know some execution has finished # curl -X POST "http://127.0.0.1:9999/notify_logger?key=x7roVhBsiZ18Dg3DX3iCm9pXhXdbZWx2&hash=0x9b945af23f0701cddcdb7dabcd72c9c7ffd3a155ace237a084b65460a9d36322&hash=0xa03f6ba3eb8141f0f8daee4ea016d4144f44fc4cba9e7477a4c1f041aaeb6c38&result=1" -@app.route("/notify_logger", methods=['POST']) +@app.route("/lar/notify_logger", methods=['POST']) def notif(): if request.remote_addr == '127.0.0.1': pass @@ -189,10 +189,10 @@ def notif(): logger.info("Sending extrinsic to QDAO node.") call = substrate.compose_call( call_module='ExoSys', - call_function='tool_exec_auto_report', + call_function='tool_exec_report', # TODO TBA review_report + tool_exec_invalid -> if fail to send: hash + result call_params={ 'hash': str(hash_received), - 'result': str(result_received) + 'result': str(result_received) # TBA TODO }) # Create the extrinsic itself @@ -212,11 +212,11 @@ def notif(): # Return static files -> reports static_file_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static') -@app.route('/dir', methods=['GET']) +@app.route('/lar/dir', methods=['GET']) def serve_dir_directory_index(): return send_from_directory(static_file_dir, 'index.html') -@app.route('/dir/', methods=['GET']) +@app.route('/lar/dir/', methods=['GET']) def serve_file_in_dir(path): if not os.path.isfile(os.path.join(static_file_dir, path)): path = os.path.join(path, 'index.html') @@ -226,7 +226,7 @@ def serve_file_in_dir(path): # Get user data as json, ID: substrate address # http://127.0.0.1:9999/auditors -@app.route('/auditors', methods=['GET']) +@app.route('/lar/auditors', methods=['GET']) def get_auditors(): conn = sqlite3.connect(dbFile) c = conn.cursor() @@ -239,7 +239,7 @@ def get_auditors(): # Get auditor data based on address # http://127.0.0.1:9999/auditor-data?address=5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY -@app.route('/auditor-data', methods=['GET']) +@app.route('/lar/auditor-data', methods=['GET']) def get_auditordata(): address = request.args.get('address', default = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", type = str) @@ -250,13 +250,15 @@ def get_auditordata(): c = conn.cursor() c.execute('SELECT * FROM auditors WHERE address = "%s"' % address) auditorData = c.fetchall() + column_names = [description[0] for description in c.description] + dict_rows = [dict(zip(column_names, row)) for row in auditorData] conn.close() - return json.dumps(auditorData) + return json.dumps(dict_rows) # Called at signup and profile update: Button click -> open polkadotJS -> sign message -> POST to API # VCN, verify from chain needed - eg. must be POST ss58 address == ss58 signer - needs to be changed in prod # Test: curl -X POST -H "Content-type: application/json" -d "{\"address\" : \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\", \"profileHash\" : \"0x001x1x0\", \"name\" : \"n4me\", \"picUrl\" : \"userx.me/a.ng\", \"webUrl\" : \"nmexxa.org\", \"bio\" : \"Teh Bio of user aX\"}" "127.0.0.1:9999/profile_update" -@app.route('/profile_update', methods=['POST']) +@app.route('/lar/profile_update', methods=['POST']) def profileUpdate(): content_type = request.headers.get('Content-Type') if (content_type == 'application/json'): @@ -281,6 +283,8 @@ def profileUpdate(): conn = sqlite3.connect(dbFile) c = conn.cursor() + #TBA fix this awful double quickfix + c.execute("INSERT INTO auditors VALUES ('{}', 'x', 'x', 'x', 'x', 'x', 'x')".format(profile_data['address'])) c.execute('UPDATE auditors SET profileHash = "{}", name = "{}", picUrl = "{}", webUrl = "{}", bio = "{}" WHERE address = "{}"'.format(profile_data['profileHash'], profile_data['name'], profile_data['picUrl'], profile_data['webUrl'], profile_data['bio'], profile_data['address'])) conn.commit() conn.close() @@ -290,19 +294,21 @@ def profileUpdate(): # List all audit requests from API DB # 127.0.0.1:9999/audit-requests -@app.route('/audit-requests', methods=['GET']) +@app.route('/lar/audit-requests', methods=['GET']) def get_auditRequests(): conn = sqlite3.connect(dbFile) c = conn.cursor() c.execute('SELECT * FROM auditStates') auditRequests = c.fetchall() + column_names = [description[0] for description in c.description] + dict_rows = [dict(zip(column_names, row)) for row in auditRequests] conn.close() - return json.dumps(auditRequests) + return json.dumps(dict_rows) # Request audit to be saved to DB # Accepted in dev, but this needs to check blockchain data because we only want to save those entries that are valid/paid, eg VCN # Test: curl -X POST -H "Content-type: application/json" -d "{\"requestor\" : \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\", \"hash\" : \"0x001x1x0\", \"projectUrl\" : \"urleraX\"}" 127.0.0.1:9999/request-audit -@app.route('/request-audit', methods=['POST']) +@app.route('/lar/request-audit', methods=['POST']) def requestAudit(): content_type = request.headers.get('Content-Type') if (content_type == 'application/json'): @@ -332,7 +338,7 @@ def requestAudit(): # Take an audit # Test: curl -X POST -H "Content-type: application/json" -d "{\"audit_taker\" : \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\",\"audit_hash\" : \"0x001x1x0\"}" 127.0.0.1:9999/take_audit -@app.route('/take_audit', methods=['POST']) +@app.route('/lar/take_audit', methods=['POST']) def takeAudit(): content_type = request.headers.get('Content-Type') if (content_type == 'application/json'): @@ -358,9 +364,37 @@ def takeAudit(): else: return 'Content-Type not supported!' -# Send report (1 audit can have multiple reports sent) +# Challenge an audit +# Test: curl -X POST -H "Content-type: application/json" -d "{\"challenger\" : \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\",\"audit_hash\" : \"0x0\"}" 127.0.0.1:9999/challenge_audit +@app.route('/lar/challenge_audit', methods=['POST']) +def challenge_audit(): + content_type = request.headers.get('Content-Type') + if (content_type == 'application/json'): + challenge_audit = request.json + + # Check input + try: + if check_address(challenge_audit['challenger']): pass + else: return DontHakit + if check_address(challenge_audit['audit_hash']): pass + else: return DontHakit + except: + return DontHakit + + conn = sqlite3.connect(dbFile) + c = conn.cursor() + c.execute('UPDATE auditStates SET challenger = "{}" WHERE hash = "{}"'.format(challenge_audit['challenger'],challenge_audit['audit_hash'])) + conn.commit() + c.execute('SELECT * FROM auditStates') + auditRequests = c.fetchall() + conn.close() + return json.dumps(auditRequests) + else: + return 'Content-Type not supported!' + +# Send report (1 audit can have multiple reports sent), to be verified # Test: curl -X POST -H "Content-type: application/json" -d "{\"reportUrl\" : \"urlx\",\"audit_hash\" : \"0x001x1x0\"}" 127.0.0.1:9999/send_report -@app.route('/send_report', methods=['POST']) +@app.route('/lar/send_report', methods=['POST']) def sendReport(): content_type = request.headers.get('Content-Type') if (content_type == 'application/json'): @@ -388,7 +422,7 @@ def sendReport(): # Get reports # http://127.0.0.1:9999/get-reports -@app.route('/get-reports', methods=['GET']) +@app.route('/lar/get-reports', methods=['GET']) def get_report(): conn = sqlite3.connect(dbFile) c = conn.cursor() diff --git a/exotools/temp.db b/exotools/temp.db index 19474d6..15a77c0 100644 Binary files a/exotools/temp.db and b/exotools/temp.db differ diff --git a/frontend/substrate-front-end-template/public/assets/main.css b/frontend/substrate-front-end-template/public/assets/main.css index 8dc9dc0..8adb684 100644 --- a/frontend/substrate-front-end-template/public/assets/main.css +++ b/frontend/substrate-front-end-template/public/assets/main.css @@ -44,11 +44,11 @@ font-family: 'Quicksand' } -.selectBox div { +.selectBox .auditorDiv { border: 2px solid #ffffff00; } -.selectBox div:hover{ +.selectBox .auditorDiv:hover{ /* background-color: #f7f7f7; */ cursor:pointer; border: 2px solid #f7f7f7; @@ -71,3 +71,8 @@ .selectBox::-webkit-scrollbar-thumb:hover { background: #017ed8; } + +/* Hide react errors */ +body > iframe { + display: none; +} \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/About.js b/frontend/substrate-front-end-template/src/About.js index 4d98bc6..1c457e9 100644 --- a/frontend/substrate-front-end-template/src/About.js +++ b/frontend/substrate-front-end-template/src/About.js @@ -1,32 +1,35 @@ import React from 'react' -import { Container, Segment, Header } from 'semantic-ui-react' +import { useNavigate } from "react-router-dom"; +import { Container, Segment, Header, Button, Icon } from 'semantic-ui-react' export default function About(props) { + const navigate = useNavigate(); return ( -
About
+
Introduction
-

QRUCIAL DAO is a system for trustless audits, and certification using non-transferable NFTs, - exogenous tooling and decentralized Consensus.

-

To us, it is ironic that web3 and trustless systems are trusting - web2 auditors and legacy security companies to protect - them from threat actors. This is the reason we want to build - a system in which the community and the projects can trust - that the work in fact has been done. Often, security audits - of web3 projects are performed in a way that relies on in - transparency and a blind trust in a company logo.

-

For example, no one verifies that the right tools have been - used for the job or that the auditor performing the task is - knowledgeable enough to perform the work professionally. - Our project provides you a transparent on chain solution to - this. We provide on chain tools which are developed by Qrucial as well as the community to test your project in a transparent and scalable way. - After this phase, you can choose from a pool of verified auditors who already proved their skills in on chain CTFs and - through our on chain reputation system.

-

In the end you get the report as non-transferable NFT which - is bound to the audited smart contract itself and therefore - verifiable and transparent.

-
+

QRUCIAL DAO is a web3 security toolsuite and reputation system. Currently the project is in beta test mode.

+

Why? We find it ironic that web3 and trustless systems are trusting web2 auditors and legacy security companies to protect them from threat actors. This is the reason we want to build + a system in which the community and the projects can trust that quality work in fact has been done.

+

For instance, no one verifies that the right tools have been used for the job or that the auditor performing the task is experienced enough. + Our project provides you a transparent on chain solution to this problem. We provide a transparent system where it is clear who did a thorough audit and who did shallow work.

+

All audit reports are bound to the audited project packages themselves and results are verifiable and transparent. Audit reports can be challenged, so the better hackers can steal the reputation of others.

+
Request audit
+

Get QRD and request an automated audit, tools will run automatically. And someone might even do a free test audit for you in this beta instance, just for fun.

+ +
Become an auditor
+

Right now, in this test instance, anyone can "hack" him/herself to become an auditor. In the live system we'll add those who already proved their skills through CTF games, eg. CCTF.

+ +
Become council member
+

Council members overview the audit challenges and help the QDAO system to be fair play. Become a council member by hacking the beta system or asking on matrix.

+
) diff --git a/frontend/substrate-front-end-template/src/AccountSelector.js b/frontend/substrate-front-end-template/src/AccountSelector.js index 334f47b..46a0382 100644 --- a/frontend/substrate-front-end-template/src/AccountSelector.js +++ b/frontend/substrate-front-end-template/src/AccountSelector.js @@ -141,7 +141,7 @@ export function BalanceAnnotation(props) { return () => unsubscribe && unsubscribe() }, [api, currentAccount]) - const mUnitBalance = (+(+balanceString / 100000).toFixed()).toLocaleString('en-US') + const mUnitBalance = (+(+balanceString / 1000000).toFixed()).toLocaleString('en-US') return currentAccount ? ( props.label ? @@ -150,7 +150,9 @@ export function BalanceAnnotation(props) { {accountBalance} : - {mUnitBalance} mQRD + + {mUnitBalance} MQRD + ) : null } diff --git a/frontend/substrate-front-end-template/src/App.js b/frontend/substrate-front-end-template/src/App.js index e4f5fcb..26a384b 100644 --- a/frontend/substrate-front-end-template/src/App.js +++ b/frontend/substrate-front-end-template/src/App.js @@ -22,6 +22,7 @@ import Home from './Home' import TopMenu from './TopMenu' import CouncilPage from './CouncilPage' import About from './About' +import Requestor from './Requestor' function Main() { const { apiState, apiError, keyringState } = useSubstrateState() @@ -36,10 +37,10 @@ function Main() { @@ -76,7 +77,7 @@ function Main() { }> }> - Under construction }> + }> }> }> diff --git a/frontend/substrate-front-end-template/src/ApproveAuditor.js b/frontend/substrate-front-end-template/src/ApproveAuditor.js index cbc1d91..4bc31b8 100644 --- a/frontend/substrate-front-end-template/src/ApproveAuditor.js +++ b/frontend/substrate-front-end-template/src/ApproveAuditor.js @@ -22,7 +22,7 @@ export default function ApproveAuditor(props) { setMinScore(minScore.toJSON()) }, [api]) - const score = details && JSON.parse(details).score + const score = details && details.score const isAllowed = score >= minScore ? true : false const approvee = currentAccount?.address @@ -30,12 +30,15 @@ export default function ApproveAuditor(props) { let signedUps = [] const getData= async()=>{ - await fetch('/auditors', { + await fetch('/lar/auditors', { headers : { 'Content-Type': 'application/json', 'Accept': 'application/json' } }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } return response.json() }).then(data => { data.forEach(auditor => { diff --git a/frontend/substrate-front-end-template/src/AuditList.js b/frontend/substrate-front-end-template/src/AuditList.js index 872ada4..e40a5c2 100644 --- a/frontend/substrate-front-end-template/src/AuditList.js +++ b/frontend/substrate-front-end-template/src/AuditList.js @@ -1,63 +1,51 @@ -import React, { useState, useEffect, useRef } from 'react' +import React, { useState } from 'react' import BasicModal from './BasicModal' +import { SendReportButton } from './ReportButton.js' export default function AuditList(props) { - const [auditData, setAuditData] = useState([]) - - const getData=()=>{ - fetch('/audit-requests', { - headers : { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }).then(response => { - return response.json() - } - ).then(data =>{ - setAuditData(data) - }).catch((err) => { - console.log(err.message) - }) - } - - const initialRender = useRef(true); - useEffect(()=>{ - if (initialRender.current) { - getData() - initialRender.current = false; - } else { - setTimeout(() => { - getData() - }, 1000) - } - },[props.auditsChange]) - const [modalOpen, setModalOpen] = useState(false) const [modalValue, setModalValue] = useState('') const handleClick = props.handleClick - + const setState = props.setState + function AuditElem(props) { const audit = props.elem; + const onClick = props.onClick || + function(audit) { + setModalOpen(true); + setModalValue({ + content: audit, + header: audit.projectUrl + }); + handleClick && handleClick(audit); + } + return ( -
+
{ - setModalOpen(true); - setModalValue({ - content: audit, - header: audit.projectUrl - }); - handleClick && handleClick(audit); - }} - >{audit.projectUrl} -
+ style={{padding: '5px', cursor: 'pointer', wordBreak: 'break-word'}} + onClick={() => onClick(audit)} + > + {audit.hash} +
+ {props.reportButton && + + } + ) } - const list = auditData.map((a, i) => ) + const list = props.auditData.map((a, i) => ( + ) + ) return (
diff --git a/frontend/substrate-front-end-template/src/AuditRequests.js b/frontend/substrate-front-end-template/src/AuditRequests.js index 5d0e44d..2c41aae 100644 --- a/frontend/substrate-front-end-template/src/AuditRequests.js +++ b/frontend/substrate-front-end-template/src/AuditRequests.js @@ -1,13 +1,93 @@ -import { useState } from 'react' -import { Grid, Input, Button } from 'semantic-ui-react' +import { useState, useEffect, useRef } from 'react' +import { Grid, Input, Button, Header } from 'semantic-ui-react' +import toast from 'react-hot-toast' +import { useSubstrateState } from './substrate-lib' import AuditList from './AuditList' export default function AuditRequests(props) { - const [selected, setSelected] = useState([]) + const [selected, setSelected] = useState({ hash: ''}) + const { currentAccount } = useSubstrateState() + + const [auditData, setAuditData] = useState([]) + const [auditsChange, setAuditsChange] = useState('') + + const getData=()=>{ + fetch('/lar/audit-requests', { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + setAuditData(data) + }).catch((err) => { + console.log(err.message) + }) + } + + const initialRender = useRef(true); + useEffect(()=>{ + if (initialRender.current) { + getData() + initialRender.current = false; + } else { + setTimeout(() => { + getData() + }, 1000) + } + },[auditsChange]) const handleChange = audit => setSelected(audit) + const sendData= async(postData)=>{ + await fetch('/lar/take_audit', { + method: 'POST', + body: JSON.stringify(postData), + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + if (data.Error || data[0]?.Error || + (typeof data === 'string' && data.startsWith('Error'))) + toast.error('Error: save failed') + else toast.success('Data saved') + setSelected({ hash: '' }) + setAuditsChange(postData) + }).catch((err) => { + toast.error("ERROR " + err.message) + }) + } + + useEffect(()=>{ + setAuditsChange(currentAccount?.address) + },[currentAccount]) + + const onClick = () => { + const audit_taker = currentAccount?.address + const audit_hash = selected.hash + const signature = currentAccount.sign(audit_hash) + const isValid = currentAccount.verify(audit_hash, signature, currentAccount.publicKey) + if (isValid) sendData({ audit_taker, audit_hash }) + else toast.error('Signature is not valid') + } + + const nonTakenAudits = auditData.filter(a => a.topAuditor?.length <= 16) + + const myAudits = auditData.filter(a => a.topAuditor === currentAccount.address) + const description = props.reports ? 'you can choose the report you would like to challenge.' : 'you can choose the audit to take.' @@ -15,21 +95,47 @@ export default function AuditRequests(props) { return ( - - - - -

You can view the details of the audit by clicking on the item. - Also by clicking on it {description} -

-
- setSelected([])}> -
- -
-
+ + + {nonTakenAudits.length > 0 && + + } + + +

You can view the details of the audit by clicking on the item. + Also by clicking on it {description} +

+
+ setSelected({hash: ''})}> +
+ +
+
+
+ + {myAudits?.length > 0 && + +
+ Your audits +
+ +
+ } +
) } \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/AuditorForm.js b/frontend/substrate-front-end-template/src/AuditorForm.js index 4739008..1358e33 100644 --- a/frontend/substrate-front-end-template/src/AuditorForm.js +++ b/frontend/substrate-front-end-template/src/AuditorForm.js @@ -5,6 +5,7 @@ import toast from 'react-hot-toast' import useTxAndPost from './hooks/useTxAndPost' import { useSubstrateState } from './substrate-lib' +import useFormValidation from './hooks/useFormValidation' export default function AuditorForm(props) { const { currentAccount } = useSubstrateState() @@ -12,8 +13,9 @@ export default function AuditorForm(props) { const auditorFields = auditorData ? {...auditorData} : { name:'', picUrl: '', webUrl: '', bio: '', address: currentAccount?.address } delete auditorFields.profileHash const [formState, setFormState] = useState(auditorFields) + const [disabled, setDisabled] = useState(false) - const postAttrs = { postUrl: '/profile_update' } + const postAttrs = { postUrl: '/lar/profile_update' } const txAttrs = { palletRpc: 'auditModule', callable: props.method, finishEvent: props.finishEvent } const { txAndPost } = useTxAndPost(txAttrs, postAttrs) @@ -41,7 +43,11 @@ export default function AuditorForm(props) { return (
- + { props.method === 'updateProfile' && } + trigger={} > @@ -19,22 +33,20 @@ export default function CancelAccount(props) { - - setOpen(false)} - attrs={{ - palletRpc: 'auditModule', - callable: 'cancelAccount', - inputParams: [], - paramFields: [], - }} - /> +
diff --git a/frontend/substrate-front-end-template/src/ChallengeReportButton.js b/frontend/substrate-front-end-template/src/ChallengeReportButton.js new file mode 100644 index 0000000..d5d4e2a --- /dev/null +++ b/frontend/substrate-front-end-template/src/ChallengeReportButton.js @@ -0,0 +1,97 @@ +import React, { useState } from 'react' +import { Modal, Button, Form } from 'semantic-ui-react' +import useTxAndPost from './hooks/useTxAndPost' +import { useSubstrateState } from './substrate-lib' +import { RemoveVuln, AddVuln, PatchVuln } from './ChallengeReportInputs' + +export default function ChallengeReportButton(props) { + const [open, setOpen] = useState(false) + const { currentAccount } = useSubstrateState() + + const removeInitial = [{ removeId: "" }] + const addInitial = [{ tool: "", addClass: "", addRisk: "", addDescrip: "" }] + const patchInitial = [{ patchId: "", patchClass: "", patchRisk: "", patchDescrip: "" }] + const [remove, setRemove] = useState(removeInitial) + const [add, setAdd] = useState(addInitial) + const [patch, setPatch] = useState(patchInitial) + + const removeParam = remove?.map(r => r.removeId).filter(id => id) + const addParam = add?.map((a) => [a.tool, a.addClass, a.addRisk, a.addDescrip]) + .filter((a) => { return a[0] || a[1] || a[2] || a[3] }) + const patchParam = patch?.map((p) => [p.patchId, p.patchClass, p.patchRisk, p.patchDescrip]) + .filter((a) => { return a[0] || a[1] || a[2] || a[3] }) + + const closeClear = () => { + setOpen(false) + setRemove(removeInitial) + setAdd(addInitial) + setPatch(patchInitial) + } + + const postAttrs = { postUrl: '/lar/challenge_audit' } + const txAttrs = { palletRpc: 'exoSys', callable: 'challengeReport', finishEvent: closeClear } + + const { txAndPost } = useTxAndPost(txAttrs, postAttrs) + + const onClick = async (event, data) => { + const inputParams = [ + props.auditHash, + props.reportId, + removeParam, + addParam, + patchParam + ] + const postData = { audit_hash: props.auditHash, challenger: currentAccount.address } + txAndPost(inputParams, postData) + } + + const [disabledRem, setDisabledRem] = useState(false) + const [disabledAdd, setDisabledAdd] = useState(false) + const [disabledPatch, setDisabledPatch] = useState(false) + const disabled = disabledRem || disabledAdd || disabledPatch + + return ( +
+ setOpen(true)} + open={open} + trigger={ + + } + > + + + + + + + + + + + + + + +
+ ) +} diff --git a/frontend/substrate-front-end-template/src/ChallengeReportInputs.js b/frontend/substrate-front-end-template/src/ChallengeReportInputs.js new file mode 100644 index 0000000..c023553 --- /dev/null +++ b/frontend/substrate-front-end-template/src/ChallengeReportInputs.js @@ -0,0 +1,286 @@ +import React from 'react' +import { Button, Form, Input, Dropdown, Header, Divider } from 'semantic-ui-react' +import useFormValidation from './hooks/useFormValidation' + +export function RemoveVuln(props){ + const data = props.data + const setData = props.setData + + const handleClick=()=>{ + setData([...data, { removeId: "" }]) + } + + const handleChange=(e,i)=>{ + const {name,value}= e.target + const onchangeVal = [...data] + onchangeVal[i][name] = value + setData(onchangeVal) + } + + const handleDelete=(i)=>{ + const deleteVal = [...data] + deleteVal.splice(i,1) + setData(deleteVal) + } + + const { + // disabled, + handleBlur, + showError, + ErrorLabel, + } = useFormValidation(data, props.setDisabled) + + return ( +
+
+
+ Remove vulnerabilities +
+ +
+ { + data.map((val,i) => +
+ + handleChange(e,i)} + onBlur={handleBlur('removeId' + i)} + /> + + + + +
+ ) + } +
+ ) +} + +export function AddVuln(props){ + const data = props.data + const setData = props.setData + + const handleClick=()=>{ + setData([...data, { tool: "", addClass: "", addRisk: "", addDescrip: "" }]) + } + + const handleChange=(e,i)=>{ + const {name,value}= e.target + const onchangeVal = [...data] + onchangeVal[i][name] = value + setData(onchangeVal) + } + + const handleDelete=(i)=>{ + const deleteVal = [...data] + deleteVal.splice(i,1) + setData(deleteVal) + } + + const toolOptions = [ + { key: 'Manual', text: 'Manual', value: 'Manual' }, + { key: 'Clippy', text: 'Clippy', value: 'Clippy' }, + { key: 'CargoAudit', text: 'CargoAudit', value: 'CargoAudit' }, + { key: 'Octopus', text: 'Octopus', value: 'Octopus' } + ] + + const { + // disabled, + handleBlur, + showError, + ErrorLabel, + } = useFormValidation(data, props.setDisabled) + + return ( +
+
+
+ Add vulnerabilities +
+ +
+ { + data.map((val,i) => +
+ + { + const onchangeVal = [...data] + onchangeVal[i].tool = dropdown.value + setData(onchangeVal) + }} + value={data.addTool} + name='addTool' + /> + + + + handleChange(e,i)} + onBlur={handleBlur('addClass' + i)} + /> + + + + handleChange(e,i)} + onBlur={handleBlur('addRisk' + i)} + /> + + + + handleChange(e,i)} + onBlur={handleBlur('addDescrip' + i)} + /> + + + +
+ ) + } +
+ ) +} + +export function PatchVuln(props){ + const data = props.data + const setData = props.setData + + const handleClick=()=>{ + setData([...data, { patchId: "", patchClass: "", patchRisk: "", patchDescrip: "" }]) + } + + const handleChange=(e,i)=>{ + const {name,value}= e.target + const onchangeVal = [...data] + onchangeVal[i][name] = value + setData(onchangeVal) + } + + const handleDelete=(i)=>{ + const deleteVal = [...data] + deleteVal.splice(i,1) + setData(deleteVal) + } + + const { + // disabled, + handleBlur, + showError, + ErrorLabel, + } = useFormValidation(data, props.setDisabled) + + return ( +
+
+
+ Patch vulnerabilities +
+ +
+ { + data.map((val,i) => +
+ + handleChange(e,i)} + onBlur={handleBlur('patchId' + i)} + /> + + + + + handleChange(e,i)} + onBlur={handleBlur('patchClass' + i)} + /> + + + + handleChange(e,i)} + onBlur={handleBlur('patchRisk' + i)} + /> + + + + handleChange(e,i)} + onBlur={handleBlur('patchDescrip' + i)} + /> + + + +
+ ) + } +
+ ) +} \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/Challenges.js b/frontend/substrate-front-end-template/src/Challenges.js new file mode 100644 index 0000000..15534cc --- /dev/null +++ b/frontend/substrate-front-end-template/src/Challenges.js @@ -0,0 +1,198 @@ +import React, { useEffect, useState } from 'react' +import { Segment, List, Header } from 'semantic-ui-react' + +import { useSubstrateState } from './substrate-lib' +import ViewChallenge from './ViewChallenge' + +export default function Challenges(props) { + const { api, currentAccount } = useSubstrateState() + const [auditHashes, setAuditHashes] = useState([]) + const [challenge, setChallenge] = useState('') + + useEffect(() => { + const query = async () => { + const challenges = await api.query.exoSys.challenges.entries() + let challengedHashes =[] + challenges.forEach(([key, value]) => { + const hash = key.toHuman()[0] + challengedHashes.push(hash) + }); + const hashes = [...new Set(challengedHashes)]; + setAuditHashes(hashes) + } + if (currentAccount) { + query() + } + }, [api, currentAccount]) + + const [reportsState, setReportsState] = useState([]) + + const getReportData = () => { + let auditReports = [] + auditHashes.forEach(auditHash => { + const query = async () => { + await api.query.exoSys.reports( + auditHash, + (result) => { + if (!result.isNone) { + const res = JSON.parse(result.toString()) + res.forEach((report, index) => { + report.auditHash = auditHash + report.reportId = index + const vulnQuery = async (auditHash, index) => { + await api.query.exoSys.vulnerabilities( + auditHash, index, + (result) => { + const vulns = result.isNone ? null : result.toHuman(); + Object.assign(report, { vulnerabilities: vulns }) + auditReports.push(report) + setReportsState(auditReports.slice()) + } + )} + vulnQuery(auditHash, index) + }) + } + }) + } + query() + }) + } + + useEffect(()=>{ + getReportData() + },[auditHashes]) + + const [challengesState, setChallengesState] = useState([]) + + useEffect(() => { + let challengeData = [] + reportsState.forEach(report => { + const query = async (report) => { + await api.query.exoSys.challenges( + report.auditHash, report.reportId, + (result) => { + if (result.isNone) return + const res = result.toHuman() + res.forEach((chall, index) => { + chall.auditHash = report.auditHash + chall.reportId = report.reportId + chall.challengeId = index + const existingIndex = challengeData.findIndex((elem) => { + return (elem.auditHash === report.auditHash && + elem.reportId === report.reportId && + elem.auditor === chall.auditor) + }) + if (existingIndex > -1) challengeData[existingIndex] = chall + else challengeData.push(chall) + }) + setChallengesState(challengeData.slice()) + }) + } + if (currentAccount) { + query(report) + } + }) + }, [reportsState, currentAccount]) + + const [auditState, setAuditState] = useState([]) + + useEffect(() => { + fetch('/lar/audit-requests', { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + setAuditState(data) + }).catch((err) => { + console.log(err.message) + }) + },[]) + + const [formState, setFormState] = useState({}) + + const handleClick = (ch) => { + setFormState({ + threshold: 1, + proposal: '', + hash: ch.auditHash, + reportId: ch.reportId, + challengeId: ch.challengeId, + lengthBound: 5000 + }) + setChallenge(ch) + } + + const [auditorNames, setAuditorNames] = useState([]) + useEffect(() => { + const names = auditorNames.slice() + challengesState.map((ch, i) => { + const getData = async()=>{ + await fetch('/lar/auditor-data?' + new URLSearchParams({ address: ch.auditor }), { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + }).then(data =>{ + names[i] = (data[0]).name + setAuditorNames(names.slice()) + }).catch((err) => { + console.log(err.message) + }) + } + getData() + }) + },[challengesState]) + + const ChallengeTitles = () => { + return challengesState.map((ch, i) => { + const audit = auditState.find(a => a.hash === ch.auditHash) + const auditorName = auditorNames[i] + return ( + + handleClick(ch)}> + {audit?.hash} / reportId: {ch.reportId}. + Challenger: {auditorName || ch.auditor} + State: {ch.state} + + + ) + }) + } + + return ( +
+ +
List of challenges
+
+ + + +
+
+ {challenge && + + + + } +
+ ) +} \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/CouncilPage.js b/frontend/substrate-front-end-template/src/CouncilPage.js index 687a3b8..b51cb1f 100644 --- a/frontend/substrate-front-end-template/src/CouncilPage.js +++ b/frontend/substrate-front-end-template/src/CouncilPage.js @@ -1,43 +1,48 @@ import React, { useEffect, useState } from 'react' -import { Segment } from 'semantic-ui-react' +import { Segment, Header } from 'semantic-ui-react' import { useSubstrateState } from './substrate-lib' -import ApproveAuditor from './ApproveAuditor' +import Challenges from './Challenges' export default function CouncilPage(props) { const { api, currentAccount } = useSubstrateState() const [details, setDetails] = useState(null) - + useEffect(() => { let unsub = null const query = async () => { - unsub = await api.query.auditModule.auditorMap( - currentAccount.address, + unsub = await api.query.challengeCouncil.members( + undefined, (result) => { result.isNone ? setDetails(null) : setDetails(result.toString()) } ) } - if (api?.query?.auditModule?.auditorMap && currentAccount) { + if (currentAccount) { query() } return () => unsub && unsub() }, [api, currentAccount]) - const isAuditor = details ? true : false + const isCouncil = details?.includes(currentAccount.address) ? true : false return (
- {isAuditor === false && -
Sign up to be an auditor first!
+ {isCouncil === false && +
+

You are not member of the council yet.

+

+ + How to become a council member? + +

+
} - {isAuditor && ( + {isCouncil && (
- - - - -

Verify challenge

+ +
Verify Challenge
+
)} diff --git a/frontend/substrate-front-end-template/src/CreateProposal.js b/frontend/substrate-front-end-template/src/CreateProposal.js new file mode 100644 index 0000000..c057a5e --- /dev/null +++ b/frontend/substrate-front-end-template/src/CreateProposal.js @@ -0,0 +1,207 @@ +import React, { useState, useEffect } from 'react' +import { Segment, Header, Form, Dropdown, Button } from 'semantic-ui-react' +import { web3FromSource } from '@polkadot/extension-dapp' +import BN from 'bn.js' +import toast from 'react-hot-toast' + +import { useSubstrateState } from './substrate-lib' +import { createToast } from './toastContent' +import Events from './template-comps/Events.js' + +export default function CreateProposal(props) { + const { api, currentAccount } = useSubstrateState() + const initial = { + threshold: 1, + proposal: '', + hash: '', + reportId: '', + challengeId: '', + lengthBound: 5000, + } + const formState = props.formState + const setFormState = props.setFormState + const [unsub, setUnsub] = useState(null) + + const [eventFeed, setEventFeed] = useState([]) + const [showEvent, setShowEvent] = useState() + const setStatus = createToast() + + useEffect(() => { + const events = eventFeed.map((e, i) => +

{e.summary}
) + if (events.length) setStatus(events) + }, [showEvent]) + + const getFromAcct = async () => { + const { + address, + meta: { source, isInjected }, + } = currentAccount + if (!isInjected) { + return [currentAccount] + } + const injector = await web3FromSource(source) + return [address, { signer: injector.signer }] + } + const showStatus = (status) => { + status.isFinalized + ? setStatus(`😉 Finalized. Block hash: ${status.asFinalized.toString()}`) + : setStatus(`Current transaction status: ${status.type}`) + } + const txErrHandler = err => setStatus(`😞 Transaction Failed: ${err.toString()}`) + + const signedTx = async () => { + const fromAcct = await getFromAcct() + const unsub = await api.tx.challengeCouncil.propose + ( formState.threshold, + api.tx.exoSys[formState.proposal](formState.hash, formState.reportId, formState.challengeId), + formState.lengthBound + ) + .signAndSend(...fromAcct, + ({ status, dispatchError }) => { + showStatus(status) + // as subscribed, every status change calls the callback fn + if (!dispatchError && status.isInBlock) { + setFormState(initial) + setShowEvent([formState.hash, formState.reportId, formState.challengeId]) + } + if (dispatchError && status.isFinalized) { + if (dispatchError.isModule) { + // We have to convert the error to the required format from the type what we get + // needs revision at substrate updates + const origError = dispatchError.asModule + const index = origError.index + const error = new BN(origError.error[0]) + // for module errors, we have the section indexed, lookup + const decoded = api.registry.findMetaError({ index, error }); + const { docs, name, section } = decoded; + toast.error(`${section}.${name}:\n ${docs.join(' ')}`); + } else { + // Other, CannotLookup, BadOrigin, no extra info + toast.error(dispatchError.toString()); + } + } + } + ) + .catch(txErrHandler) + + setUnsub(() =>unsub) + } + + const onClick = async (event, data) => { + if (!props.details?.includes(currentAccount.address)) { + toast.error('No permission with this account') + return + } + if (typeof unsub === 'function') { + unsub() + setUnsub(null) + } + signedTx() + } + + const extOptions = [ + { key: 'approve', text: 'Approve Challenge', value: 'approveChallenge' }, + { key: 'reject', text: 'Reject Challenge', value: 'rejectChallenge' }, + { key: 'remove', text: 'Remove Challenge', value: 'removeChallenge' }, + ] + + const handleChange = (_, data) => { + setFormState(prev => ({ ...prev, [data.name]: data.value })) + } + +/* const { + // disabled, + handleBlur, + showError, + ErrorLabel, + } = useFormValidation(formState) */ + + return ( +
+ +
Create proposal
+
+{/* + + + + + + + */} + + + +{/* + + + + + + + + + + + + */} +
+
+ +
+ ) +} diff --git a/frontend/substrate-front-end-template/src/Home.js b/frontend/substrate-front-end-template/src/Home.js index 317bd52..2f1b246 100644 --- a/frontend/substrate-front-end-template/src/Home.js +++ b/frontend/substrate-front-end-template/src/Home.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { Grid, Header, Segment } from 'semantic-ui-react' import RequestAudit from './RequestAudit' @@ -7,9 +7,41 @@ import AuditorPool from './AuditorPool' import AuditList from './AuditList' export default function Home(props) { + const [auditData, setAuditData] = useState([]) const [auditsChange, setAuditsChange] = useState('') const changeList = (latest) => setAuditsChange(latest) + const getData=()=>{ + fetch('/lar/audit-requests', { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + setAuditData(data) + }).catch((err) => { + console.log(err.message) + }) + } + + const initialRender = useRef(true); + useEffect(()=>{ + if (initialRender.current) { + getData() + initialRender.current = false; + } else { + setTimeout(() => { + getData() + }, 1000) + } + },[auditsChange]) + return ( @@ -38,7 +70,7 @@ export default function Home(props) { Latest audits - + diff --git a/frontend/substrate-front-end-template/src/ReportButton.js b/frontend/substrate-front-end-template/src/ReportButton.js new file mode 100644 index 0000000..feda48f --- /dev/null +++ b/frontend/substrate-front-end-template/src/ReportButton.js @@ -0,0 +1,106 @@ +import React, { useState } from 'react' +import { Modal, Button, Form, Input } from 'semantic-ui-react' +import toast from 'react-hot-toast' +import useFormValidation from './hooks/useFormValidation' + +export function SendReportButton(props) { + const [formState, setFormState] = useState({ reportUrl: ''}) + const [open, setOpen] = useState(false) + + const onChange = (_, data) => { + setFormState(prev => ({ ...prev, [data.name]: data.value })) + } + + const sendData= async()=>{ + await fetch('/lar/send_report', { + method: 'POST', + body: JSON.stringify({ audit_hash: props.audit.hash, reportUrl: formState.reportUrl }), + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + if (data.Error || data[0]?.Error || + (typeof data === 'string' && data.startsWith('Error'))) + toast.error('Error: save failed') + else toast.success('Data saved') + setOpen(false) + props.setState(props.audit.hash) + }).catch((err) => { + toast.error("ERROR " + err.message) + }) + } + + const { + disabled, + handleBlur, + showError, + ErrorLabel, + } = useFormValidation(formState) + + const isDisabled = () => { + if (formState.reportUrl?.length === 0) return true + return disabled + } + + return ( +
+ setOpen(false)} + onOpen={() => setOpen(true)} + open={open} + trigger={ + + } + > + + +

Please provide the url of your report

+
+ + + + +
+
+
+ + + + +
+
+ ) +} diff --git a/frontend/substrate-front-end-template/src/Reports.js b/frontend/substrate-front-end-template/src/Reports.js new file mode 100644 index 0000000..166b369 --- /dev/null +++ b/frontend/substrate-front-end-template/src/Reports.js @@ -0,0 +1,247 @@ +import { useState, useEffect, useRef } from 'react' +import { Grid, Container, Table, List, Header } from 'semantic-ui-react' + +import { useSubstrateState } from './substrate-lib' +import AuditList from './AuditList' +import ChallengeReportButton from './ChallengeReportButton' + +export default function Reports(props) { + const { api, currentAccount } = useSubstrateState() + + const [auditData, setAuditData] = useState([]) + const [auditsChange, setAuditsChange] = useState('') + const [reports, setReports] = useState([]) + const [shownAudit, setShownAudit] = useState('') + const unsubAll = [] + const auditDetails = [] + + const getData=()=>{ + fetch('/lar/audit-requests', { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + data.forEach(audit => { + const query = async (auditHash) => { + await api.query.exoSys.reports( + auditHash, + (result) => { + const auditClone = {...audit} + if (!result.isNone) { + const reports = JSON.parse(result.toString()) + if (reports.length) { + Object.assign(auditClone, { reports }) + const existingIndex = auditDetails.findIndex((elem) => + Object.values(elem).includes(auditHash)) + if (existingIndex > -1) auditDetails[existingIndex] = auditClone + else auditDetails.push(auditClone) + } + } + setAuditData(auditDetails.slice()) + }) + .then(unsub => unsubAll.push(unsub)) + } + query(audit.hash) + }) + }).catch((err) => { + console.log(err.message) + }) + } + +const setReportData = (auditData) => { + let totalReports = [] + auditData.forEach((audit => { + let auditsReports = [] + audit.reports.forEach((report, i) => { + const query2 = async (auditHash, reportId) => { + await api.query.exoSys.vulnerabilities( + auditHash, reportId, + (result) => { + const vulns = result.isNone ? null : result.toHuman() + auditsReports[reportId] = Object.assign({}, report, { reportId, vulnerabilities: vulns }) + const auditReportsRecord = { auditHash: auditHash, reports : auditsReports } + const existingIndex = totalReports.findIndex((elem) => + Object.values(elem).includes(auditHash)) + if (existingIndex > -1) totalReports[existingIndex] = auditReportsRecord + else totalReports.push(auditReportsRecord) + setReports(totalReports.slice()) + } + ) + } + query2(audit.hash, i) + }) + })) +} + + const initialRender = useRef(true); + useEffect(()=>{ + if (initialRender.current) { + getData() + initialRender.current = false; + } else { + setTimeout(() => { + getData() + }, 1000) + } + },[auditsChange]) + + const handleChange = null + + useEffect(()=>{ + setAuditsChange(currentAccount?.address) + },[currentAccount]) + + useEffect(()=>{ + setReportData(auditData) + },[auditData]) + + const audits = auditData + + const onAuditClick = function(audit) { + setShownAudit(audit) + } + + const auditHash = shownAudit?.hash + const reportsOfAudit = auditHash ? + reports.find(elem => elem.auditHash === auditHash)?.reports + : [] + + return ( + + +

+

You can view the details of the audit and its report by clicking on the item.

+
+ + {(reports.length > 0) && + + } + + +
+ {shownAudit && +
+ + + + Project Url + + {shownAudit.projectUrl} + + + + State + + {shownAudit.state} + + + + Auto Report + + {shownAudit.autoReport} + + + + Manual Report + + {shownAudit.manualReport} + + + +
+ +
Reports
+ { reportsOfAudit?.map((rep, index) => + + + + auditor + + {rep.auditor} + + + + kind + + {rep.kind} + + + { rep.vulnerabilities && + + vulnerabilities + + } + { rep.vulnerabilities?.map((vuln,i) => + + {i} + + + + + tool + + {vuln.tool} + + + + + + classification + + {vuln.classification} + + + + + + risk + + {vuln.risk} + + + + + + description + + {vuln.description} + + + + + + + )} + {(rep.auditor !== currentAccount.address) && + + + + + + } + +
+ )} +
+ } +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/RequestAudit.js b/frontend/substrate-front-end-template/src/RequestAudit.js index 5aea85d..b651fca 100644 --- a/frontend/substrate-front-end-template/src/RequestAudit.js +++ b/frontend/substrate-front-end-template/src/RequestAudit.js @@ -5,26 +5,27 @@ import { useSubstrateState } from './substrate-lib' import useTxAndPost from './hooks/useTxAndPost' import useFormValidation from './hooks/useFormValidation' -const DEFAULT_STAKE = 500 +export const DEFAULT_STAKE = 500 export default function RequestAudit(props) { - const [formState, setFormState] = useState({ url: '', hash: '', stake: DEFAULT_STAKE }) - const { url, hash, stake } = formState + const [formState, setFormState] = useState({ url: '', hash: '', bounty: DEFAULT_STAKE, minAuditorScore: DEFAULT_STAKE }) + const { url, hash, bounty, minAuditorScore } = formState const { currentAccount } = useSubstrateState() const requestor = currentAccount?.address const finishEvent = () => { props.changeList(url) - setFormState({ url: '', hash: '', stake: DEFAULT_STAKE }) + setFormState({ url: '', hash: '', bounty: DEFAULT_STAKE, minAuditorScore: DEFAULT_STAKE }) } - const postAttrs = { postUrl: '/request-audit' } - const txAttrs = { palletRpc: 'exoSys', callable: 'toolExecReq', finishEvent } + const postAttrs = { postUrl: '/lar/request-audit' } + const txAttrs = { palletRpc: 'exoSys', callable: 'requestReview', finishEvent } const { txAndPost } = useTxAndPost(txAttrs, postAttrs) const onClick = async (event, data) => { - const txData = [url, hash, stake] - const postData = { requestor, projectUrl: formState.url }; + const h256Hash = hash.startsWith('0x') ? hash : '0x' + hash + const txData = [url, h256Hash, bounty, minAuditorScore] + const postData = { requestor, hash: h256Hash, projectUrl: formState.url }; txAndPost(txData, postData) } @@ -49,14 +50,14 @@ export default function RequestAudit(props) {
- + setAuditsChange(latest) + + const getData=()=>{ + fetch('/lar/audit-requests', { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + } + ).then(data =>{ + setAuditData(data) + }).catch((err) => { + console.log(err.message) + }) + } + + const initialRender = useRef(true); + useEffect(()=>{ + if (initialRender.current) { + getData() + initialRender.current = false; + } else { + setTimeout(() => { + getData() + }, 1000) + } + },[auditsChange]) + + useEffect(()=>{ + setAuditsChange(currentAccount?.address) + },[currentAccount]) + + const myAudits = auditData.filter(a => a.requestor === currentAccount.address) + + return ( + + + +

The minimum stake for requesting an audit is {DEFAULT_STAKE} QRD. +

+ Your current balance is: +

+ How to get QRD? +

+
+ {(myAudits.length > 0) && + +
My requests
+ +
+ } +
+ ) +} diff --git a/frontend/substrate-front-end-template/src/TopMenu.js b/frontend/substrate-front-end-template/src/TopMenu.js index d5c958f..c6a9fd3 100644 --- a/frontend/substrate-front-end-template/src/TopMenu.js +++ b/frontend/substrate-front-end-template/src/TopMenu.js @@ -25,10 +25,10 @@ export default function TopMenu() { - - - - + + + + diff --git a/frontend/substrate-front-end-template/src/ViewChallenge.js b/frontend/substrate-front-end-template/src/ViewChallenge.js new file mode 100644 index 0000000..213c762 --- /dev/null +++ b/frontend/substrate-front-end-template/src/ViewChallenge.js @@ -0,0 +1,177 @@ +import React, { useState, useEffect } from 'react' +import { Grid, Header, List } from 'semantic-ui-react' +import CreateProposal from './CreateProposal' + +export default function ViewChallenge(props) { + const challenge = props.challenge + const report = props.reports?.find((elem) => { + return ( + elem.auditHash === challenge.auditHash && + elem.reportId === challenge.reportId) + }) + const auditUrl = props.auditState.find(a => + a.hash === challenge.auditHash)?.projectUrl + + const getData = async(address, setState )=>{ + await fetch('/lar/auditor-data?' + new URLSearchParams({ address }), { + headers : { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } + return response.json() + }).then(data =>{ + setState(data[0]) + }).catch((err) => { + console.log(err.message) + }) + } + + const [reportAuditor, setReportAuditor] = useState('') + const [challengeAuditor, setChallengeAuditor] = useState('') + useEffect(() => { + getData(report?.auditor, setReportAuditor) + getData(challenge?.auditor, setChallengeAuditor) + },[report, challenge]) + + return ( +
+ {report && + + +
Report
+ + + audit url: {auditUrl} + + + auditor: {reportAuditor?.name || report.auditor} + + + report kind: {report.kind} + + {report.vulnerabilities && + + vulnerabilities: + {report.vulnerabilities?.map((vuln, i) => + + + ID: {i}. + + + tool: + {report.vulnerabilities[i].tool} + + + classification: + {report.vulnerabilities[i].classification} + + + risk: + {report.vulnerabilities[i].risk} + + + description: + {report.vulnerabilities[i].description} + + + )} + + } + + +
+ + +
Challenge
+ + + audit url: {auditUrl} + + + auditor: {challengeAuditor?.name || challenge.auditor} + + + state: {challenge.state} + + {(challenge.removeVulnerabilities.length > 0)&& + + Remove Vulnerabilities: + {(challenge.removeVulnerabilities.length > 0) && + challenge.removeVulnerabilities.map((vuln, i) => + + + Vulnerability ID: + {challenge.removeVulnerabilities[i]}. + + + )} + + } + {(challenge.addVulnerabilities.length > 0) && + + Add Vulnerabilities: + {challenge.addVulnerabilities?.map((vuln, i) => + + + tool: + {challenge.addVulnerabilities[i].tool} + + + classification: + {challenge.addVulnerabilities[i].classification} + + + risk: + {challenge.addVulnerabilities[i].risk} + + + description: + {challenge.addVulnerabilities[i].description} + + + )} + + } + {(challenge.patchVulnerabilities.length > 0) && + + Patch Vulnerabilities: + {challenge.patchVulnerabilities?.map((vuln, i) => + + + vulnerability Id: + {challenge.patchVulnerabilities[i].vulnerabilityId}. + + + classification: + {challenge.patchVulnerabilities[i].classification} + + + risk: + {challenge.patchVulnerabilities[i].risk} + + + description: + {challenge.patchVulnerabilities[i].description} + + + )} + + } + + { (challenge.state === 'Pending') && + + } +
+
+ } +
+ ) +} diff --git a/frontend/substrate-front-end-template/src/hooks/useFormValidation.js b/frontend/substrate-front-end-template/src/hooks/useFormValidation.js index 3a72da1..bf9fcdb 100644 --- a/frontend/substrate-front-end-template/src/hooks/useFormValidation.js +++ b/frontend/substrate-front-end-template/src/hooks/useFormValidation.js @@ -1,27 +1,56 @@ import React, { useState, useEffect } from 'react' -function useFormValidation(formState) { +function useFormValidation(origFormState, setDisabledState) { + let formState = {} + if (Array.isArray(origFormState)) { + const newArray = origFormState.map((elem) => Object.assign({}, elem)) + newArray.forEach((elem, i) => { + Object.keys(elem).forEach(key => { + delete Object.assign(elem, {[key + i]: elem[key] })[key] + }) + Object.assign(formState, elem) + }) + } else formState = origFormState + const initialState = {} Object.keys(formState).forEach(key => initialState[key] = false) const [errors, setErrors] = useState(initialState) const [touched, setTouched] = useState(initialState) const [disabled, setDisabled] = useState(true) + function allowedChars(str) { + if (str === '') return true + else if (!str) return false + const regex = /^[A-Za-z0-9\s-:.,/?_&#=]*$/ + return regex.test(str) + } + + const isValidUrl = (urlString) => { + const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string + '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator + return !!urlPattern.test(urlString); + } + function validate(formState) { const entries = Object.entries(formState) const errors = {} for (const [key, value] of entries) { switch (key) { - case 'url': { - const parts = value.split('.'); - const extension = parts.length > 1 ? parts.pop().toLowerCase() : ''; - errors.url = extension === 'tar' ? false : true - } + case 'url': + case 'reportUrl': + case 'webUrl': + case 'picUrl': + errors[key] = isValidUrl(value) ? false : true break case 'hash': - errors.hash = value.length === 64 ? false : true + errors.hash = value.length > 63 ? false : true break default: + errors[key] = allowedChars(value) ? false : true break } } @@ -30,9 +59,11 @@ function useFormValidation(formState) { useEffect(() => { const errors = validate(formState) - setDisabled(Object.values(errors).some(v => v)) + const isDisabled = Object.values(errors).some(v => v) + setDisabled(isDisabled) + setDisabledState && setDisabledState(isDisabled) setErrors(errors) - }, [formState]) + }, [origFormState]) const handleBlur = field => e => { setTouched(prev => ({ ...prev, [field]: true })) diff --git a/frontend/substrate-front-end-template/src/hooks/useTxAndPost.js b/frontend/substrate-front-end-template/src/hooks/useTxAndPost.js index 45ba0d6..1f0a60b 100644 --- a/frontend/substrate-front-end-template/src/hooks/useTxAndPost.js +++ b/frontend/substrate-front-end-template/src/hooks/useTxAndPost.js @@ -19,13 +19,19 @@ export default function useTxAndPost(txAttrs, postAttrs) { 'Accept': 'application/json' } }).then(response => { + if (!response.ok) { + throw Error(response.status + ' ' + response.statusText) + } return response.json() } ).then(data =>{ + if (data.Error || data[0]?.Error || + (typeof data === 'string' && data.startsWith('Error'))) + toast.error('Error: save failed') + else toast.success('Data saved') resCallback && typeof resCallback === 'function' && resCallback(data) - toast.success('Data saved') }).catch((err) => { - toast.error("ERROR" + err.message) + toast.error("ERROR " + err.message) }) } @@ -46,7 +52,7 @@ export default function useTxAndPost(txAttrs, postAttrs) { const setStatus = createToast() const showStatus = (status) => { status.isFinalized - ? setStatus(`😉 Finalized. Block hash: ${status.asFinalized.toString()}`) + ? setStatus(`Finalized. Block hash: ${status.asFinalized.toString()}`) : setStatus(`Current transaction status: ${status.type}`) } const txErrHandler = err => setStatus(`😞 Transaction Failed: ${err.toString()}`) @@ -64,7 +70,7 @@ export default function useTxAndPost(txAttrs, postAttrs) { sendData(postData) finishEvent && typeof finishEvent === 'function' && finishEvent() } - if (dispatchError && status.isFinalized) { + if (dispatchError && status.isInBlock) { if (dispatchError.isModule) { // We have to convert the error to the required format from the type what we get // needs revision at substrate updates diff --git a/frontend/substrate-front-end-template/src/semantic-ui/site/globals/site.variables b/frontend/substrate-front-end-template/src/semantic-ui/site/globals/site.variables index f3353df..c380ca0 100644 --- a/frontend/substrate-front-end-template/src/semantic-ui/site/globals/site.variables +++ b/frontend/substrate-front-end-template/src/semantic-ui/site/globals/site.variables @@ -25,3 +25,6 @@ @solidSelectedBorderColor : rgba(0, 147, 255, 0.3); @focusedFormBorderColor : rgba(0, 147, 255, 0.35); + +@linkColor : #017ed8; +@linkHoverColor : lighten( @linkColor, 5); \ No newline at end of file diff --git a/frontend/substrate-front-end-template/src/substrate-lib/components/TxButton.js b/frontend/substrate-front-end-template/src/substrate-lib/components/TxButton.js index 1150e5d..9c48f26 100644 --- a/frontend/substrate-front-end-template/src/substrate-lib/components/TxButton.js +++ b/frontend/substrate-front-end-template/src/substrate-lib/components/TxButton.js @@ -64,7 +64,7 @@ function TxButton({ const setStatusState = (status) => { status.isFinalized - ? setStatus(`😉 Finalized. Block hash: ${status.asFinalized.toString()}`) + ? setStatus(`Finalized. Block hash: ${status.asFinalized.toString()}`) : setStatus(`Current transaction status: ${status.type}`) } @@ -116,7 +116,7 @@ function TxButton({ ({ status, dispatchError }) => { setStatusState(status) // as subscribed, every status change calls the callback fn - if (dispatchError && status.isFinalized) { + if (dispatchError && status.isInBlock) { if (dispatchError.isModule) { // We have to convert the error to the required format from the type what we get // needs revision at substrate updates diff --git a/frontend/substrate-front-end-template/src/template-comps/Events.js b/frontend/substrate-front-end-template/src/template-comps/Events.js index e4f91d4..53df9c6 100644 --- a/frontend/substrate-front-end-template/src/template-comps/Events.js +++ b/frontend/substrate-front-end-template/src/template-comps/Events.js @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from 'react' -import { Feed, Grid, Button } from 'semantic-ui-react' - +import React, { useEffect, /* useState */ } from 'react' +import { /* Feed, */ Grid, /* Button */ } from 'semantic-ui-react' import { useSubstrateState } from '../substrate-lib' // Events to be filtered from feed @@ -13,7 +12,9 @@ const eventParams = ev => JSON.stringify(ev.data) function Main(props) { const { api } = useSubstrateState() - const [eventFeed, setEventFeed] = useState([]) +/* const [eventFeed, setEventFeed] = useState([]) + */ + const setEventFeed = props.setEventFeed useEffect(() => { let unsub = null @@ -52,12 +53,12 @@ function Main(props) { return () => unsub && unsub() }, [api.query.system]) - const { feedMaxHeight = 250 } = props - + /* const { feedMaxHeight = 250 } = props + */ return ( -

Events

-